// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.collide.client.code.autocomplete.html;

import static com.google.collide.codemirror2.TokenType.ATTRIBUTE;
import static com.google.collide.codemirror2.TokenType.TAG;

import javax.annotation.Nonnull;

import com.google.collide.client.code.autocomplete.CodeAnalyzer;
import com.google.collide.client.util.collections.StringMultiset;
import com.google.collide.codemirror2.CodeMirror2;
import com.google.collide.codemirror2.Token;
import com.google.collide.codemirror2.TokenType;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.TaggableLine;
import com.google.collide.shared.util.JsonCollections;

/**
 * Analyzes token stream and builds or updates {@link HtmlTagWithAttributes}.
 *
 * <p>For each line we hold:<ol>
 * <li> {@link HtmlTagWithAttributes} unfinished at the beginning of line
 * <li> {@link HtmlTagWithAttributes} unfinished at the end of line
 * <li> list of attributes added to (1) in this line
 * </ol>
 *
 * <p>When line is reparsed:<ol>
 * <li> remove attributes from the list from appropriate tag and clean list
 * <li> set unfinished beginning tag equal to the ending tag of previous line
 * <li> add attributes to list until tag closes
 * <li> build and set unfinished tag at the end of line
 * </ol>
 *
 * <p> That way, during completion we have 2 cases:<ul>
 * <li> for tag that starts and ends in this line we need to parse it's content
 * <li> for tag that is unfinished (starts in previous lines or end in the
 * following lines) we already have parsed tag information.
 * </ul>
 *
 */
public class XmlCodeAnalyzer implements CodeAnalyzer {

  static final String TAG_START_TAG = HtmlAutocompleter.class.getName() + ".startTag";
  static final String TAG_END_TAG = HtmlAutocompleter.class.getName() + ".endTag";
  private static final String TAG_ATTRIBUTES = HtmlAutocompleter.class.getName() + ".attributes";

  @Override
  public void onBeforeParse() {
    // Do nothing.
  }

  @Override
  public void onParseLine(
      TaggableLine previousLine, TaggableLine line, @Nonnull JsonArray<Token> tokens) {
    processLine(previousLine, line, tokens);
  }

  static void processLine(
      TaggableLine previousLine, TaggableLine line, @Nonnull JsonArray<Token> tokens) {
    // Ignore case always, as HTML == XmlCodeAnalyzer for now.
    final boolean ignoreCase = true;

    clearLine(line);
    HtmlTagWithAttributes tag = previousLine.getTag(TAG_END_TAG);
    line.putTag(TAG_START_TAG, tag);

    int index = 0;
    int size = tokens.size();

    boolean inTag = false;
    int lastTagTokenIndex = -1;

    if (tag != null) {
      inTag = true;
      boolean newAttributes = false;
      JsonArray<String> attributes = line.getTag(TAG_ATTRIBUTES);
      if (attributes == null) {
        newAttributes = true;
        attributes = JsonCollections.createArray();
      }

      StringMultiset tagAttributes = tag.getAttributes();

      while (index < size) {
        Token token = tokens.get(index);
        index++;
        TokenType tokenType = token.getType();
        if (ATTRIBUTE == tokenType) {
          String attribute = token.getValue();
          attribute = ignoreCase ? attribute.toLowerCase() : attribute;
          attributes.add(attribute);
          tagAttributes.add(attribute);
        } else if (TAG == tokenType) {
          // Tag closing token
          tag.setDirty(false);
          inTag = false;
          break;
        }
      }
      if (newAttributes && attributes.size() != 0) {
        line.putTag(TAG_ATTRIBUTES, attributes);
      } else if (!newAttributes && attributes.size() == 0) {
        line.putTag(TAG_ATTRIBUTES, null);
      }
    } else {
      line.putTag(TAG_ATTRIBUTES, null);
    }

    while (index < size) {
      Token token = tokens.get(index);
      index++;
      TokenType tokenType = token.getType();
      if (TAG == tokenType) {
        if (inTag) {
          if (">".equals(token.getValue()) || "/>".equals(token.getValue())) {
            // If type is "tag" and content is ">", this is HTML token.
            inTag = false;
          }
        } else {
          // Check that we are in html mode.
          if (CodeMirror2.HTML.equals(token.getMode())) {
            lastTagTokenIndex = index - 1;
            inTag = true;
          }
        }
      }
    }

    if (inTag) {
      if (lastTagTokenIndex != -1) {
        index = lastTagTokenIndex;
        Token token = tokens.get(index);
        index++;
        String tagName = token.getValue().substring(1).trim();
        tag = new HtmlTagWithAttributes(tagName);
        StringMultiset tagAttributes = tag.getAttributes();
        while (index < size) {
          token = tokens.get(index);
          index++;
          TokenType tokenType = token.getType();
          if (ATTRIBUTE == tokenType) {
            String attribute = token.getValue();
            tagAttributes.add(ignoreCase ? attribute.toLowerCase() : attribute);
          }
        }
      }

      // In case when document ends, but last tag is not closed we state that
      // tag content is "complete" - i.e. it will not be updated further.
      if (line.isLastLine()) {
        tag.setDirty(false);
      }

      line.putTag(TAG_END_TAG, tag);
    } else {
      line.putTag(TAG_END_TAG, null);
    }
  }

  @Override
  public void onAfterParse() {
    // Do nothing.
  }

  @Override
  public void onLinesDeleted(JsonArray<TaggableLine> deletedLines) {
    for (TaggableLine line : deletedLines.asIterable()) {
      clearLine(line);
    }
  }

  private static void clearLine(TaggableLine line) {
    HtmlTagWithAttributes tag = line.getTag(TAG_START_TAG);
    if (tag == null) {
      return;
    }
    tag.setDirty(true);
    JsonArray<String> attributes = line.getTag(TAG_ATTRIBUTES);
    if (attributes == null) {
      return;
    }
    tag.getAttributes().removeAll(attributes);
    attributes.clear();
  }
}
