// 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 static com.google.collide.shared.document.util.LineUtils.comparePositions;

import java.util.HashMap;
import java.util.Map;

import com.google.collide.client.util.PathUtil;
import com.google.collide.dto.CodeBlock;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;

/**
 * Encapsulates code structure data of the file opened in the editor.
 *
 */
class CodeFile {
  static class CodeBlockReferences {
    private final Map<CodeBlock, CodeBlock> childParentRefs = new HashMap<CodeBlock, CodeBlock>();

    void addChildParentRef(CodeBlock child, CodeBlock parent) {
      childParentRefs.put(child, parent);
    }

    CodeBlock getParent(CodeBlock child) {
      return childParentRefs.get(child);
    }

    void clear() {
      childParentRefs.clear();
    }
  }

  private static Scope findScope(Scope scope, int lineNumber, int column, boolean endInclusive) {
    int relativeToBegin =
        comparePositions(lineNumber, column, scope.getBeginLineNumber(), scope.getBeginColumn());

    // When we say that cursor column is X, we mean, that there are X chars
    // before cursor in this line.
    // But when we say that scope ends at column X, we mean that X-th char is
    // the last char that belongs to the scope.
    // That is why we do +1 to make this function work as designed.
    int scopeEndColumn = scope.getEndColumn() + 1;

    int relativeToEnd =
        comparePositions(lineNumber, column, scope.getEndLineNumber(), scopeEndColumn);
    if (relativeToBegin < 0 || relativeToEnd > 0) {
      return null;
    }
    if (!endInclusive && relativeToEnd == 0) {
      return null;
    }

    for (int i = 0; i < scope.getSubscopes().size(); ++i) {
      Scope subScope = findScope(scope.getSubscopes().get(i), lineNumber, column, endInclusive);
      if (subScope != null) {
        return subScope;
      }
    }
    return scope;
  }


  private final CodeBlockReferences refs = new CodeBlockReferences();
  private final PathUtil filePath;

  private CodeBlock rootCodeBlock;
  private Scope rootScope;

  CodeFile(PathUtil filePath) {
    Preconditions.checkNotNull(filePath);
    this.filePath = filePath;
  }

  private void buildSubscopes(Scope rootScope, CodeBlock codeBlock, JsonArray<CodeBlock> queue) {
    JsonArray<Scope> subscopes = rootScope.getSubscopes();
    for (int i = 0, size = codeBlock.getChildren().size(); i < size; i++) {
      CodeBlock child = codeBlock.getChildren().get(i);
      refs.addChildParentRef(child, codeBlock);
      if (isTextuallyNested(child, codeBlock)) {
        Scope childScope = new Scope(child);
        subscopes.add(childScope);
      } else {
        queue.add(child);
      }
    }
    for (int i = 0; i < subscopes.size(); i++) {
      Scope child = subscopes.get(i);
      buildSubscopes(child, child.getCodeBlock(), queue);
    }
  }

  private boolean isTextuallyNested(CodeBlock child, CodeBlock parent) {
    return
            LineUtils.comparePositions(child.getStartLineNumber(), child.getStartColumn(),
                parent.getStartLineNumber(), parent.getStartColumn()) >= 0
        &&
            LineUtils.comparePositions(child.getEndLineNumber(), child.getEndColumn(),
                parent.getEndLineNumber(), parent.getEndColumn()) <= 0;
  }

  /**
   * Finds the most suitable scope for a given position.
   *
   * @param lineNumber position line number
   * @param column position column
   * @param endInclusive when {@code true} then scopes are suitable if they
   *        can be expanded by adding something at the given position
   * @return scope found
   */
  Scope findScope(int lineNumber, int column, boolean endInclusive) {
    if (rootScope == null) {
      return null;
    }
    return findScope(rootScope, lineNumber, column, endInclusive);
  }

  PathUtil getFilePath() {
    return filePath;
  }

  CodeBlockReferences getReferences() {
    return refs;
  }

  CodeBlock getRootCodeBlock() {
    return rootCodeBlock;
  }

  @VisibleForTesting
  Scope getRootScope() {
    return this.rootScope;
  }

  void setRootCodeBlock(CodeBlock codeBlock) {
    this.rootCodeBlock = codeBlock;
    if (codeBlock == null) {
      return;
    }

    refs.clear();
    rootScope = new Scope(codeBlock);
    JsonArray<CodeBlock> queue = JsonCollections.createArray();
    buildSubscopes(rootScope, rootCodeBlock, queue);
    while (!queue.isEmpty()) {
      JsonArray<CodeBlock> newQueue = JsonCollections.createArray();
      for (int i = 0; i < queue.size(); i++) {
        CodeBlock queued = queue.get(i);
        Scope lexicalContainer = findScope(queued.getStartLineNumber(), queued.getStartColumn(),
            false);
        if (lexicalContainer != null) {
          Scope lexicalScope = new Scope(queued);
          lexicalContainer.getSubscopes().add(lexicalScope);
          buildSubscopes(lexicalScope, queued, newQueue);
        }
      }
      queue = newQueue;
    }
  }
}
