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

import javax.annotation.Nullable;

import com.google.collide.client.codeunderstanding.CubeClient;
import com.google.collide.client.codeunderstanding.CubeData;
import com.google.collide.client.codeunderstanding.CubeDataUpdates;
import com.google.collide.client.codeunderstanding.CubeUpdateListener;
import com.google.collide.client.util.logging.Log;
import com.google.collide.dto.CodeReference;
import com.google.collide.dto.CodeReferences;
import com.google.collide.dto.FilePosition;
import com.google.collide.dto.client.ClientDocOpFactory;
import com.google.collide.dto.client.DtoClientImpls;
import com.google.collide.json.client.Jso;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.LineNumberAndColumn;
import com.google.collide.shared.ot.PositionMigrator;
import com.google.common.annotations.VisibleForTesting;

/**
 * Storage and lookup system for cubeReferences from source ranges to
 * another file or URLs. Collects cubeReferences from various sources like Cube
 * or dynamic reference provider.
 *
 */
class ReferenceStore {

  // Tracks docops since last time we received information from Cube.
  // These docops are used when we need to find reference at given position
  // if the position has changed as the result of user edits.
  private final PositionMigrator positionMigrator;
  private final CubeClient cubeClient;
  private final CubeUpdateListener cubeListener = new CubeUpdateListener() {
    @Override
    public void onCubeResponse(CubeData data, CubeDataUpdates updates) {
      if (!updates.isFileReferences()) {
        return;
      }
      updateReferences(data);
    }
  };

  // References that come from Cube.
  private CodeReferences cubeReferences;

  // References that come from client parser.
  private DynamicReferenceProvider dynamicReferenceProvider;

  public ReferenceStore(CubeClient cubeClient) {
    this.cubeClient = cubeClient;
    this.positionMigrator = new PositionMigrator(ClientDocOpFactory.INSTANCE);
    cubeClient.addListener(cubeListener);
  }

  /**
   * Finds reference at given position.
   *
   * @param lineInfo position line info
   * @param column position column
   * @param blocking whether to block until given line is parsed for references
   * @return reference found at given position, or {@code null} if nothing there
   */
  @VisibleForTesting
  NavigableReference findReference(LineInfo lineInfo, int column, boolean blocking) {
    // TODO: Optimize this search.
    // Seach for reference at position where the cursor would have been if there were
    // no text changes.
    int line = lineInfo.number();
    NavigableReference result = null;
    if (cubeReferences != null && cubeReferences.getReferences().size() > 0) {
      // TODO: Optimize this search.
      // Seach for reference at position where the cursor would have been if there were
      // no text changes.
      LineNumberAndColumn oldPosition = positionMigrator.migrateFromNow(line, column);
      for (int i = 0; i < cubeReferences.getReferences().size(); i++) {
        CodeReference reference = cubeReferences.getReferences().get(i);
        if (reference.getReferenceStart().getLineNumber() > oldPosition.lineNumber) {
          // We've gone too far, nothing to look further.
          break;
        }
        if (isFilePositionBefore(reference.getReferenceStart(), oldPosition.lineNumber,
                oldPosition.column)
            && isFilePositionAfter(reference.getReferenceEnd(), oldPosition.lineNumber,
                oldPosition.column)) {
          // Migrate old reference to new position after edits.
          CodeReference newReference = migrateCubeReference(reference);
          if (newReference != null) {
            result = NavigableReference.createToFile(newReference);
          }
          break;
        }
      }
    }

    if (result == null && dynamicReferenceProvider != null) {
      result = dynamicReferenceProvider.getReferenceAt(lineInfo, column, blocking);
    }

    if (result == null) {
      Log.debug(getClass(), "Found no references at: (" + line + "," + column + ")");
    } else {
      Log.debug(getClass(),
          "Found reference at (" + line + "," + column + "):" + result.toString());
    }
    return result;
  }

  /**
   * Migrates old reference (its position as it was when we received cube data
   * before any edits) to new reference (new position after edits) if possible.
   * Reference cannot be migrated if some characters disappeared inside it or
   * newline was inserted in the middle.
   *
   * @param reference reference to migrate
   * @return migrated reference or {@code null} if reference cannot be migrated
   */
  private CodeReference migrateCubeReference(CodeReference reference) {
    FilePosition oldStartPosition = reference.getReferenceStart();
    FilePosition oldEndPosition = reference.getReferenceEnd();
    LineNumberAndColumn newStartPosition = positionMigrator.migrateToNow(
        oldStartPosition.getLineNumber(), oldStartPosition.getColumn());
    LineNumberAndColumn newEndPosition = positionMigrator.migrateToNow(
        oldEndPosition.getLineNumber(), oldEndPosition.getColumn());
    int newLength = newEndPosition.column - newStartPosition.column;
    int oldLength = oldEndPosition.getColumn() - oldStartPosition.getColumn();
    if (newStartPosition.lineNumber != newEndPosition.lineNumber
        || newLength != oldLength || newLength < 0) {
      // TODO: Make the method return null if text has changed inside the reference.
      return null;
    }
    DtoClientImpls.CodeReferenceImpl newReference = Jso.create().cast();
    return newReference
        .setReferenceType(reference.getReferenceType())
        .setReferenceStart(toDtoPosition(newStartPosition))
        .setReferenceEnd(toDtoPosition(newEndPosition))
            // TODO: Target may be in this file, in this case update target too.
        .setTargetStart(reference.getTargetStart())
        .setTargetEnd(reference.getTargetEnd())
        .setTargetFilePath(reference.getTargetFilePath())
        .setTargetSnippet(reference.getTargetSnippet());
  }

  private static FilePosition toDtoPosition(LineNumberAndColumn position) {
    return DtoClientImpls.FilePositionImpl.make()
        .setLineNumber(position.lineNumber)
        .setColumn(position.column);
  }

  private static boolean isFilePositionBefore(FilePosition position, int line, int column) {
    return position.getLineNumber() < line
        || (position.getLineNumber() == line && position.getColumn() <= column);
  }

  private static boolean isFilePositionAfter(FilePosition position, int line, int column) {
    return position.getLineNumber() > line
        || (position.getLineNumber() == line && position.getColumn() >= column);
  }

  private void logAllReferences() {
    if (cubeReferences == null) {
      Log.debug(getClass(), "No references info yet.");
      return;
    }
    Log.debug(getClass(), "All references in current file:");
    for (int i = 0; i < cubeReferences.getReferences().size(); i++) {
      CodeReference reference = cubeReferences.getReferences().get(i);
      Log.debug(getClass(), "reference at: " + referenceToString(reference));
    }
  }

  private static String filePositionToString(FilePosition position) {
    return "(" + position.getLineNumber() + "," + position.getColumn() + ")";
  }

  private static String referenceToString(CodeReference reference) {
    return filePositionToString(reference.getReferenceStart()) + " to file \""
        + reference.getTargetFilePath() + "\", target start: "
        + filePositionToString(reference.getTargetStart()) + ", target end: "
        + filePositionToString(reference.getTargetEnd());
  }

  public void onDocumentChanged(Document document,
      @Nullable DynamicReferenceProvider dynamicReferenceProvider) {
    updateReferences(cubeClient.getData());
    this.positionMigrator.start(document.getTextListenerRegistrar());
    this.dynamicReferenceProvider = dynamicReferenceProvider;
  }

  @VisibleForTesting
  void updateReferences(CubeData data) {
    positionMigrator.reset();
    this.cubeReferences = data.getFileReferences();
    // referenceRenderer.resetReferences(editor.getRenderer(), lineFinder);
    if (Log.isLoggingEnabled()) {
      Log.debug(getClass(), "Received code references");
      logAllReferences();
    }
  }

  public void cleanup() {
    cubeClient.removeListener(cubeListener);
    positionMigrator.stop();
  }
}
