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

import com.google.collide.json.shared.JsonArray;
import com.google.collide.json.shared.JsonStringMap;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorManager;
import com.google.collide.shared.util.JsonCollections;
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.StringUtils;

// TODO: need the preferred newline characters for the doc
/**
 * Document model for the code editor.
 *
 *  The document is modeled using a linked list of lines. (This allows for very fast line insertions
 * and still good performance for other common editor operations.)
 *
 * During a text change, listeners will be called in this order:
 * <ul>
 * <li>{@link Anchor.ShiftListener}</li>
 * <li>{@link Anchor.RemoveListener}</li>
 * <li>{@link LineCountListener}</li>
 * <li>{@link LineListener}</li>
 * <li>{@link TextListener}</li>
 * </ul>
 */
public class Document implements DocumentMutator {

  /**
   * A listener that is called when the number of lines in the document changes.
   * 
   * See the callback ordering documented in {@link Document}. 
   */
  public interface LineCountListener {
    void onLineCountChanged(Document document, int lineCount);
  }

  /**
   * A listener that is called when a line is added or removed from the
   * document.
   *
   * Note: In the case of a multiline insertion/deletion, this will be called
   * once.
   * 
   * See the callback ordering documented in {@link Document}. 
   */
  public interface LineListener {
    /**
     * @param lineNumber the line number of the first item in {@code addedLines}
     * @param addedLines a contiguous list of lines that were added
     */
    void onLineAdded(Document document, int lineNumber, JsonArray<Line> addedLines);

    /**
     * @param lineNumber the previous line number of the first item in
     *        {@code removedLines}
     * @param removedLines a contiguous list of (now detached) lines that were
     *        removed
     */
    void onLineRemoved(Document document, int lineNumber, JsonArray<Line> removedLines);
  }

  /**
   * A listener that is called when a text change occurs within a document.
   * 
   * See the callback ordering documented in {@link Document}. 
   */
  public interface TextListener {
    /**
     * Note: You should not mutate the document within this callback, as this is
     * not supported yet and can lead to other clients having stale position
     * information inside the {@code textChanges}.
     *
     * Note: The {@link TextChange} contains a reference to the live
     * {@link Line} from the document model. If you hold on to a reference after
     * {@link #onTextChange} returns, beware that the contents of the
     * {@link Line} could change, invalidating some of the state in the
     * {@link TextChange}.
     */
    void onTextChange(Document document, JsonArray<TextChange> textChanges);
  }

  /**
   * A listener which is called before any changes are actually made to the
   * document and any anchors are moved.
   */
  public interface PreTextListener {
    /**
     * Note: You should not mutate the document within this callback, as this is
     * not supported yet and can lead to other clients having stale position
     * information inside the {@code textChanges}.
     *
     * <p>
     * This callback is called synchronously with document mutations, the less
     * work you can do the better.
     *
     * @param line The line the text change will take place on.
     * @param lineNumber The line number of the line.
     * @param column The column the text change will start at.
     * @param text The text which is either being inserted or deleted.
     * @param type The type of {@link TextChange} that will be occurring.
     */
    void onPreTextChange(Document document,
        TextChange.Type type,
        Line line,
        int lineNumber,
        int column,
        String text);
  }

  public static Document createEmpty() {
    return new Document();
  }

  public static Document createFromString(
      String contents) {
    Document doc = createEmpty();
    doc.insertText(doc.getFirstLine(), 0, 0, contents);

    return doc;
  }
  
  private static int idCounter = 0;

  private final AnchorManager anchorManager;

  private Line firstLine;

  private Line lastLine;

  private int lineCount = 1;

  private final ListenerManager<LineListener> lineListenerManager;

  private final ListenerManager<LineCountListener> lineCountListenerManager;

  private final LineFinder lineFinder;

  private final DocumentMutatorImpl documentMutator;

  private final ListenerManager<TextListener> textListenerManager;

  private final ListenerManager<PreTextListener> preTextListenerManager;

  private final int id = idCounter++;
  
  private final JsonStringMap<Object> tags = JsonCollections.createMap();

  private Document() {
    firstLine = lastLine = Line.create(this, "");
    firstLine.setAttached(true);

    anchorManager = new AnchorManager();

    documentMutator = new DocumentMutatorImpl(this);

    lineListenerManager = ListenerManager.create();

    lineCountListenerManager = ListenerManager.create();

    lineFinder = new LineFinder(this);

    textListenerManager = ListenerManager.create();

    preTextListenerManager = ListenerManager.create();
  }

  public String asText() {
    StringBuilder sb = new StringBuilder();
    for (Line line = firstLine; line != null; line = line.getNextLine()) {
      sb.append(line.getText());
    }

    return sb.toString();
  }

  @Override
  public TextChange deleteText(Line line, int column, int deleteCount) {
    return documentMutator.deleteText(line, column, deleteCount);
  }

  @Override
  public TextChange deleteText(Line line, int lineNumber, int column, int deleteCount) {
    return documentMutator.deleteText(line, lineNumber, column, deleteCount);
  }

  public AnchorManager getAnchorManager() {
    return anchorManager;
  }

  public Line getFirstLine() {
    return firstLine;
  }

  public LineInfo getFirstLineInfo() {
    return new LineInfo(firstLine, 0);
  }

  public Line getLastLine() {
    return lastLine;
  }

  public LineInfo getLastLineInfo() {
    return new LineInfo(lastLine, getLastLineNumber());
  }

  public int getLastLineNumber() {
    return lineCount - 1;
  }

  public int getLineCount() {
    return lineCount;
  }

  public ListenerRegistrar<LineCountListener> getLineCountListenerRegistrar() {
    return lineCountListenerManager;
  }

  public LineFinder getLineFinder() {
    return lineFinder;
  }

  public ListenerRegistrar<LineListener> getLineListenerRegistrar() {
    return lineListenerManager;
  }

  public String getText(Line line, int column, int count) {
    assert column < line.getText().length();

    StringBuilder s =
        new StringBuilder(StringUtils.substringGuarded(line.getText(), column, count));
    int remainingCount = count - s.length();
    line = line.getNextLine();

    while (remainingCount > 0 && line != null) {
      String capturedLineText = StringUtils.substringGuarded(line.getText(), 0, remainingCount);
      s.append(capturedLineText);
      remainingCount -= capturedLineText.length();

      line = line.getNextLine();
    }

    return s.toString();
  }

  public ListenerRegistrar<TextListener> getTextListenerRegistrar() {
    return textListenerManager;
  }

  public ListenerRegistrar<PreTextListener> getPreTextListenerRegistrar() {
    return preTextListenerManager;
  }

  @Override
  public TextChange insertText(Line line, int column, String text) {
    return documentMutator.insertText(line, column, text);
  }

  @Override
  public TextChange insertText(Line line, int lineNumber, int column, String text) {
    return documentMutator.insertText(line, lineNumber, column, text);
  }

  @Override
  public TextChange insertText(Line line, int lineNumber, int column, String text,
      boolean canReplaceSelection) {
    return documentMutator.insertText(line, lineNumber, column, text, canReplaceSelection);
  }

  @Override
  public String toString() {
    return asText();
  }

  public String asDebugString() {
    StringBuilder sb = new StringBuilder("Line count: " + getLineCount() + "\n");
    for (Line line = firstLine; line != null; line = line.getNextLine()) {
      sb.append(line.getText()).append("---\n");
    }

    return sb.toString();
  }
  
  public int getId() {
    return id;
  }
  
  /**
   * @see Line#putTag(String, Object)
   */
  public <T> void putTag(String key, T value) {
    tags.put(key, value);
  }
  
  /**
   * @see Line#getTag(String)
   */
  @SuppressWarnings("unchecked")
  public <T> T getTag(String key) {
    return (T) tags.get(key);
  }

  void commitLineCountChange(int lineCountDelta) {
    if (lineCountDelta != 0) {
      lineCount += lineCountDelta;
      lineCountListenerManager.dispatch(new Dispatcher<Document.LineCountListener>() {
        @Override
        public void dispatch(LineCountListener listener) {
          listener.onLineCountChanged(Document.this, lineCount);
        }
      });
    }
  }

  void dispatchLineAdded(final int lineNumber, final JsonArray<Line> addedLines) {
    lineListenerManager.dispatch(new Dispatcher<Document.LineListener>() {
      @Override
      public void dispatch(LineListener listener) {
        listener.onLineAdded(Document.this, lineNumber, addedLines);
      }
    });
  }

  void dispatchLineRemoved(final int lineNumber, final JsonArray<Line> removedLines) {
    lineListenerManager.dispatch(new Dispatcher<Document.LineListener>() {
      @Override
      public void dispatch(LineListener listener) {
        listener.onLineRemoved(Document.this, lineNumber, removedLines);
      }
    });
  }

  void dispatchTextChange(final JsonArray<TextChange> textChanges) {
    textListenerManager.dispatch(new Dispatcher<Document.TextListener>() {
      @Override
      public void dispatch(TextListener listener) {
        listener.onTextChange(Document.this, textChanges);
      }
    });
  }
  
  void dispatchPreTextChange(final TextChange.Type type, final Line line, final int lineNumber,
      final int column, final String text) {
    preTextListenerManager.dispatch(new Dispatcher<Document.PreTextListener>() {
      @Override
      public void dispatch(PreTextListener listener) {
        listener.onPreTextChange(Document.this, type, line, lineNumber, column, text);
      }
    });
  }

  void setFirstLine(Line line) {
    assert line != null : "Line cannot be null";
    firstLine = line;
  }

  void setLastLine(Line line) {
    assert line != null : "Line cannot be null";
    lastLine = line;
  }
}
