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

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import com.google.collide.client.util.BasicIncrementalScheduler;
import com.google.collide.client.util.IncrementalScheduler;
import com.google.collide.client.util.UserActivityManager;
import com.google.collide.codemirror2.Parser;
import com.google.collide.codemirror2.State;
import com.google.collide.codemirror2.SyntaxType;
import com.google.collide.codemirror2.Token;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.TaggableLine;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.document.TextChange;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.Anchor.RemovalStrategy;
import com.google.collide.shared.document.anchor.AnchorManager;
import com.google.collide.shared.document.anchor.AnchorType;
import com.google.collide.shared.util.ListenerManager;
import com.google.collide.shared.util.ListenerManager.Dispatcher;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.collide.shared.util.ListenerRegistrar.Remover;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;

/**
 * Parser for a document that delegates to CodeMirror.
 *
 *  This class attaches to a document and re-parses whenever the contents
 * changes. It uses an incremental parser allowing it to resume parsing from the
 * beginning of the changed line.
 *
 */
public class DocumentParser {

  public static DocumentParser create(Document document, Parser codeMirrorParser,
      UserActivityManager userActivityManager) {
    /*
     * Guess that parsing 300 lines takes 50ms, let scheduler balance actual
     * parsing time per machine.
     */
    BasicIncrementalScheduler scheduler = new BasicIncrementalScheduler(
        userActivityManager, 50, 300);
    return create(document, codeMirrorParser, scheduler);
  }

  @VisibleForTesting
  public static DocumentParser create(Document document, Parser codeMirrorParser,
      IncrementalScheduler scheduler) {
    return new DocumentParser(document, codeMirrorParser, scheduler);
  }

  /**
   * A listener that receives a callback as lines of the document get parsed.
   * Can be called synchronously with user keyboard interactions
   * or asynchronously in batch mode, parsing a few lines in a row.
   */
  public interface Listener {
    /**
     * This method is called to mark the start of asynchronous parsing
     * iteration.
     *
     * @param lineNumber number of a line iteration started from
     */
    void onIterationStart(int lineNumber);

    /**
     * This method is called to mark the finish of asynchronous parsing
     * iteration.
     */
    void onIterationFinish();

    /**
     * Note: This may be called synchronously with a user's key press, so do not
     * do too much work synchronously.
     */
    void onDocumentLineParsed(Line line, int lineNumber, @Nonnull JsonArray<Token> tokens);
  }

  private static final AnchorType PARSER_ANCHOR_TYPE = AnchorType.create(DocumentParser.class,
      "parser");

  private Anchor createParserPosition(Document document) {
    Anchor position =
        document.getAnchorManager().createAnchor(PARSER_ANCHOR_TYPE, document.getFirstLine(), 0,
            AnchorManager.IGNORE_COLUMN);
    position.setRemovalStrategy(RemovalStrategy.SHIFT);
    return position;
  }

  private final Parser codeMirrorParser;

  private final Document.TextListener documentTextListener = new Document.TextListener() {
    @Override
    public void onTextChange(Document document, JsonArray<TextChange> textChanges) {
      /*
       * Tracks the earliest change in the document, so that can be used as a
       * starting point for the parser
       */
      Line earliestLine = parserPosition.getLine();
      int earliestLineNumber = parserPosition.getLineNumber();

      for (int i = 0, n = textChanges.size(); i < n; i++) {
        TextChange textChange = textChanges.get(i);
        Line line = textChange.getLine();
        int lineNumber = textChange.getLineNumber();

        if (lineNumber < earliestLineNumber) {
          earliestLine = line;
          earliestLineNumber = lineNumber;
        }

        // Synchronously parse this line
        worker.parse(line, lineNumber, 1, null);
      }

      // Queue the earliest
      document.getAnchorManager().moveAnchor(
          parserPosition, earliestLine, earliestLineNumber, AnchorManager.IGNORE_COLUMN);

      scheduler.schedule(parserTask);
    }
  };

  private final ListenerManager<Listener> listenerManager;
  private final Anchor parserPosition;

  private final IncrementalScheduler.Task parserTask = new IncrementalScheduler.Task() {
    @Override
    public boolean run(int workAmount) {
      return executeWorker(workAmount);
    }
  };

  private final IncrementalScheduler scheduler;
  private final DocumentParserWorker worker;
  private final Remover textListenerRemover;

  private DocumentParser(
      Document document, Parser codeMirrorParser, IncrementalScheduler scheduler) {
    Preconditions.checkNotNull(codeMirrorParser);
    Preconditions.checkNotNull(scheduler);
    this.codeMirrorParser = codeMirrorParser;
    this.listenerManager = ListenerManager.create();
    this.scheduler = scheduler;
    this.worker = new DocumentParserWorker(this, codeMirrorParser);
    this.parserPosition = createParserPosition(document);
    this.textListenerRemover = document.getTextListenerRegistrar().add(documentTextListener);
  }

  /**
   * Schedules the parsing of the document from the last parsed position, or the
   * beginning of the document if this is the first time parsing.
   */
  public void begin() {
    scheduler.schedule(parserTask);
  }

  public ListenerRegistrar<Listener> getListenerRegistrar() {
    return listenerManager;
  }

  /**
   * Parses the given line synchronously, returning the tokens on the line.
   *
   * <p>This will NOT schedule parsing of subsequent lines.
   *
   * @return the parsed tokens, or {@code null} if there isn't a snapshot
   *         and it's not the first line
   */
  @Nullable
  public JsonArray<Token> parseLineSync(@Nonnull Line line) {
    return worker.parseLine(line);
  }

  /**
   * @return true if this parser mode supports smart indentation
   */
  public boolean hasSmartIndent() {
    return codeMirrorParser.hasSmartIndent();
  }

  /**
   * Return the indentation for this line, based upon the line above it.
   */
  public int getIndentation(Line line) {
    return worker.getIndentation(line);
  }

  public void teardown() {
    parserPosition.getLine().getDocument().getAnchorManager().removeAnchor(parserPosition);
    textListenerRemover.remove();
    scheduler.teardown();
  }

  void dispatchIterationStart(final int lineNumber) {
    listenerManager.dispatch(new Dispatcher<Listener>() {
      @Override
      public void dispatch(Listener listener) {
        listener.onIterationStart(lineNumber);
      }
    });
  }

  void dispatchIterationFinish() {
    listenerManager.dispatch(new Dispatcher<Listener>() {
      @Override
      public void dispatch(Listener listener) {
        listener.onIterationFinish();
      }
    });
  }

  void dispatch(final Line line, final int lineNumber, @Nonnull final JsonArray<Token> tokens) {
    listenerManager.dispatch(new Dispatcher<Listener>() {
      @Override
      public void dispatch(Listener listener) {
        listener.onDocumentLineParsed(line, lineNumber, tokens);
      }
    });
  }

  private boolean executeWorker(int workAmount) {
    dispatchIterationStart(parserPosition.getLineNumber());
    boolean result = worker.parse(parserPosition.getLine(), parserPosition.getLineNumber(),
        workAmount, parserPosition);
    dispatchIterationFinish();
    return result;
  }

  public SyntaxType getSyntaxType() {
    return codeMirrorParser.getSyntaxType();
  }

  /**
   * Checks if line has been parsed since last changes (in this line or
   * in lines above it).
   *
   * @param lineNumber number of line to check
   * @return {@code true} if line is to be parsed.
   */
  public boolean isLineDirty(int lineNumber) {
    // Without this check last line never becomes "clean".
    if (!scheduler.isBusy()) {
      return false;
    }
    return parserPosition.getLineNumber() <= lineNumber;
  }

  /**
   * Calculates parser mode at the beginning of line.
   *
   * @return {@code null} if previous line is not parsed yet
   */
  @Nullable
  public String getInitialMode(@Nonnull TaggableLine line) {
    return worker.getInitialMode(line);
  }

  /**
   * Synchronously parse beginning of the line and safely cast resulting state.
   *
   * <p>It is explicitly checked that current syntax type corresponds to
   * specified state class.
   *
   * If the parser hasn't asynchronously reached previous line (may be it is
   * appeared too recently) then {@code null} is returned.
   *
   * @see DocumentParserWorker#getParserState
   */
  public <T extends State> ParseResult<T> getState(Class<T> stateClass, Position position,
      @Nullable String appendedText) {
    Preconditions.checkArgument(getSyntaxType().checkStateClass(stateClass));
    Preconditions.checkNotNull(position);
    return worker.getParserState(position, appendedText);
  }
}
