// 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.codegraph;

import javax.annotation.Nonnull;

import com.google.collide.client.code.autocomplete.AbstractTrie;
import com.google.collide.client.code.autocomplete.PrefixIndex;
import com.google.collide.client.code.autocomplete.codegraph.js.JsCodeScope;
import com.google.collide.client.code.autocomplete.codegraph.js.JsIndexUpdater;
import com.google.collide.client.code.autocomplete.codegraph.py.PyCodeScope;
import com.google.collide.client.code.autocomplete.codegraph.py.PyIndexUpdater;
import com.google.collide.client.code.autocomplete.integration.TaggableLineUtil;
import com.google.collide.client.documentparser.ParseResult;
import com.google.collide.client.util.logging.Log;
import com.google.collide.codemirror2.State;
import com.google.collide.codemirror2.SyntaxType;
import com.google.collide.dto.CodeBlock;
import com.google.collide.dto.CodeBlock.Type;
import com.google.collide.dto.CodeGraph;
import com.google.collide.json.client.JsoStringSet;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.grok.GrokUtils;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.base.Preconditions;

/**
 * Builds a list of completion proposals from a few sources
 * (context file, external files and language constructs)
 *
 */
public class ScopeTrieBuilder {

  private static void addLexicalPrefixes(
      JsoStringSet prefixes, JsonArray<String> path, String commonSuffix) {
    for (int i = 0; i <= path.size(); i++) {
      String currentPrefix = (i == 0) ? "" : path.slice(0, i).join(".") + ".";
      prefixes.add(currentPrefix + commonSuffix);
    }
  }
  private static void addThisPrefixes(JsoStringSet prefixes, JsonArray<String> path) {
    String thisPrefix = path.slice(0, path.size() - 1).join(".") + ".";
    prefixes.add(thisPrefix);
  }

  private static void debugLog(CodeBlock codeBlock, String indent) {
    Log.debug(ScopeTrieBuilder.class, indent + CodeBlock.Type.valueOf(codeBlock.getBlockType())
        + " " + codeBlock.getName()
        + "#" + codeBlock.getId()
        + "[" + codeBlock.getStartLineNumber() + ":" + codeBlock.getStartColumn() + ","
        + codeBlock.getEndLineNumber() + ":" + codeBlock.getEndColumn() + "]");
  }

  private static void debugLogTree(CodeBlock root, String indent) {
    if (root == null) {
      Log.debug(ScopeTrieBuilder.class, "null code block");
      return;
    }
    debugLog(root, indent);
    indent = "  " + indent;
    for (int i = 0; i < root.getChildren().size(); i++) {
      debugLogTree(root.getChildren().get(i), indent);
    }
  }

  private PrefixIndex<CodeGraphProposal> externalTrie = new AbstractTrie<CodeGraphProposal>();

  private final CodeFile contextFile;

  private final SyntaxType mode;

  public ScopeTrieBuilder(CodeFile contextFile, SyntaxType mode) {
    this.contextFile = contextFile;
    this.mode = mode;
  }

  public JsoStringSet calculateScopePrefixes(CompletionContext context, Position cursor) {
    JsoStringSet result = JsoStringSet.create();
    boolean isThisContext = context.isThisContext();
    Line line = cursor.getLine();

    if (!isThisContext) {
      result.add(context.getPreviousContext());
    }

    //PY specific.
    PyCodeScope pyScope = line.getTag(PyIndexUpdater.TAG_SCOPE);
    if (pyScope != null) {
      JsonArray<String> path = PyCodeScope.buildPrefix(pyScope);
      if (isThisContext && pyScope.getType() == PyCodeScope.Type.DEF && path.size() > 1) {
        addThisPrefixes(result, path);
      } else {
        addLexicalPrefixes(result, path, context.getPreviousContext());
      }
      return result;
    }

    // Fallback - use pre-calculated results (valid at the end of line).
    JsCodeScope jsScope = line.getTag(JsIndexUpdater.TAG_SCOPE);

    @SuppressWarnings("unchecked")
    // Trying to get results up to cursor position.
    ParseResult<State> parseResult = context.getParseResult();
    if (parseResult != null) {
      jsScope = JsIndexUpdater.calculateContext(
          TaggableLineUtil.getPreviousLine(line), parseResult.getTokens()).getScope();
    }

    if (jsScope != null) {
      JsonArray<String> path = JsCodeScope.buildPrefix(jsScope);
      if (isThisContext && path.size() > 1) {
        addThisPrefixes(result, path);
      } else {
        addLexicalPrefixes(result, path, context.getPreviousContext());
      }
    }

    int lineNumber = cursor.getLineNumber();
    int column = cursor.getColumn();
    column = Math.max(0, column - context.getPreviousContext().length());
    final Scope scope = contextFile.findScope(lineNumber, column, true);

    // Can't calculate scope or matching codeBlock or it is root scope.
    if (scope == null || scope == contextFile.getRootScope()) {
      return result;
    }

    CodeBlock codeBlock = scope.getCodeBlock();
    JsonArray<String> prefix = buildPrefix(codeBlock);
    // Add prefixes corresponding to outer scopes.
    if (isThisContext && Type.VALUE_FUNCTION == codeBlock.getBlockType() && prefix.size() > 1) {
      addThisPrefixes(result, prefix);
    } else {
      addLexicalPrefixes(result, prefix, context.getPreviousContext());
    }
    return result;
  }

  /**
   * Builds sequence on names that represents path to given {@link CodeBlock}.
   *
   * <p>Given block name is the last item in resulting sequence.
   */
  private JsonArray<String> buildPrefix(@Nonnull CodeBlock codeBlock) {
    Preconditions.checkNotNull(codeBlock);

    CodeBlock current = codeBlock;
    JsonArray<String> prefix = JsonCollections.createArray();
    while (current != null && Type.VALUE_FILE != current.getBlockType()) {
      prefix.add(current.getName());
      current = contextFile.getReferences().getParent(current);
    }
    prefix.reverse();

    return prefix;
  }

  public void setCodeGraph(CodeGraph codeGraph) {
    CodeBlock contextFileCodeBlock = GrokUtils.findFileCodeBlock(codeGraph,
        contextFile.getFilePath().getPathString());
    contextFile.setRootCodeBlock(contextFileCodeBlock);
    externalTrie = new CodeGraphPrefixIndex(codeGraph, mode, contextFile.getFilePath());
  }

  public PrefixIndex<CodeGraphProposal> getCodeGraphTrie() {
    Preconditions.checkNotNull(externalTrie);
    return externalTrie;
  }
}
