// 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.shared.ot;

import com.google.collide.dto.DocOp;
import com.google.collide.dto.shared.DocOpFactory;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.Document.TextListener;
import com.google.collide.shared.document.LineNumberAndColumn;
import com.google.collide.shared.document.TextChange;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerRegistrar;

/**
 * A mechanism that translates file positions to/from file position at
 * earlier point in time when this position migrator was created or reset.
 * The scope of PositionMigrator object is currently opened document.
 * On "start" position migrator starts tracking text changes.
 * Calling "reset" clears tracked docops and moves tracking start to current
 * moment.
 *
 */
public class PositionMigrator {

  private final TextListener textListener = new TextListener() {
    @Override
    public void onTextChange(Document document, JsonArray<TextChange> textChanges) {
      PositionMigrator.this.onTextChange(textChanges);
    }
  };

  private final DocOpFactory docOpFactory;
  private final JsonArray<DocOp> appliedDocOps = JsonCollections.createArray();
  private ListenerRegistrar.Remover textListenerRemover;

  public PositionMigrator(DocOpFactory docOpFactory) {
    this.docOpFactory = docOpFactory;
  }

  /**
   * Converts given position as it was at the time when tracking started,
   * to current position.
   *
   * @param lineNumber old position line number
   * @param column old position column
   * @return current position line number and column
   */
  public LineNumberAndColumn migrateToNow(int lineNumber, int column) {
    // TODO: Cache the result.
    DocOp docOp = composeCurrentDocOps();
    if (docOp == null) {
      return LineNumberAndColumn.from(lineNumber, column);
    }
    PositionTransformer positionTransformer =
        new PositionTransformer(lineNumber, column);
    positionTransformer.transform(docOp);
    return LineNumberAndColumn.from(positionTransformer.getLineNumber(),
        positionTransformer.getColumn());
  }

  public boolean haveChanges() {
    return composeCurrentDocOps() != null;
  }

  /**
   * Converts given position at current time to position as it was at the time
   * when tracking started.
   *
   * @param lineNumber current position line number
   * @param column current position column
   * @return old position line number and column
   */
  public LineNumberAndColumn migrateFromNow(int lineNumber, int column) {
    // TODO: Cache the result.
    DocOp docOp = composeCurrentDocOps();
    if (docOp == null) {
      return LineNumberAndColumn.from(lineNumber, column);
    }
    PositionTransformer positionTransformer =
        new PositionTransformer(lineNumber, column);
    positionTransformer.transform(Inverter.invert(docOpFactory, docOp));
    return LineNumberAndColumn.from(positionTransformer.getLineNumber(),
        positionTransformer.getColumn());
  }

  /**
   * Forgets about all currently recorded text changes and continues tracking
   * if started.
   */
  public void reset() {
    appliedDocOps.clear();
  }

  /**
   * Starts tracking text changes. Text changes collected so far are discarded.
   *
   * @param textListenerRegistrar listener registrar to use for listening text
   *        changes
   */
  public void start(ListenerRegistrar<TextListener> textListenerRegistrar) {
    reset();
    stop();
    this.textListenerRemover = textListenerRegistrar.add(textListener);
  }

  /**
   * Stops tracking text changes, keeping currently recorded text changes.
   */
  public void stop() {
    if (textListenerRemover != null) {
      textListenerRemover.remove();
      textListenerRemover = null;
    }
  }

  private void onTextChange(JsonArray<TextChange> textChanges) {
    try {
      appliedDocOps.add(DocOpUtils.createFromTextChanges(docOpFactory, textChanges));
    } catch (Composer.ComposeException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Composes currently collected doc ops into a single doc op and returns it.
   *
   * @return composed doc op or {@code null} if no doc ops were collected so far
   */
  private DocOp composeCurrentDocOps() {
    if (appliedDocOps.size() < 2) {
      return appliedDocOps.size() > 0 ? appliedDocOps.get(0) : null;
    }
    try {
      DocOp docOp = Composer.compose(docOpFactory, appliedDocOps.asIterable());
      appliedDocOps.clear();
      appliedDocOps.add(docOp);
      return docOp;
    } catch (Composer.ComposeException e) {
      throw new RuntimeException(e);
    }
  }
}
