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

import static com.google.common.base.Preconditions.checkArgument;

import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.util.ListenerManager;
import com.google.collide.shared.util.ListenerManager.Dispatcher;
import com.google.collide.shared.util.ListenerRegistrar;

// TODO: resolve the looseness in type safety
/*
 * There is loosened type safety in this class for the ListenerManagers to allow
 * for both the read-only interface (ReadOnlyAnchor) and this class to share the
 * same underlying ListenerManager for each event.
 */
/**
 * Model for an anchor in the document that maintains its up-to-date positioning
 * in the document as text changes occur.
 *
 * An anchor should pass {@link AnchorManager#IGNORE_LINE_NUMBER} if its line
 * number will not be used.
 *
 * Similarly, an anchor should pass {@link AnchorManager#IGNORE_COLUMN} if its
 * column will not be used. Anchors with this flag will be called
 * "line anchors".
 *
 * One caveat with line anchors that have the {@link RemovalStrategy#REMOVE}
 * removal strategy is they will be removed when the line's preceding newline
 * character is removed. For example, if the cursor is at (line 2, column 0) and
 * the user presses backspace, line 2's contents will be appended to line 1. In
 * this case, any line anchors on line 2 will be removed. One example where this
 * might be confusing is if the user selects from (line 1, column 0) to (line 2,
 * column 0) and presses delete. This would delete the line anchors on line 2
 * with the {@link RemovalStrategy#REMOVE} strategy even though the user has
 * logically deleted the text on line 1. There used to be a partial exception to
 * this rule to account for this confusin case, but that led to subtle bugs.
 *
 * Anchors can be positioned through the {@link AnchorManager}.
 *
 */
@SuppressWarnings("rawtypes")
public class Anchor implements ReadOnlyAnchor {

  /**
   * Listener that is notified when an anchor is shifted as a result of a text
   * change that affects the line number or column of the anchor. If the anchor
   * ignores the column, it will never receive a callback as a result of a
   * column shift. The same is true for line numbers.
   * 
   * <p>
   * If the anchor moves because of
   * {@link AnchorManager#moveAnchor(Anchor, Line, int, int)}, this will not be
   * called (see {@link MoveListener}).
   */
  public interface ShiftListener extends ShiftListenerImpl<Anchor> {
  }
  
  interface ShiftListenerImpl<A extends ReadOnlyAnchor> {
    void onAnchorShifted(A anchor);
  }

  /**
   * Listener that is notified when an anchor is moved via
   * {@link AnchorManager#moveAnchor(Anchor, Line, int, int)}.
   * 
   * <p>
   * If the anchor shifts because of text changes in the document, this will not
   * be called (see {@link ShiftListener}.
   */
  public interface MoveListener extends MoveListenerImpl<Anchor> {
  }
  
  interface MoveListenerImpl<A extends ReadOnlyAnchor> {
    void onAnchorMoved(A anchor);
  }
  /**
   * Listener that is notified when an anchor is removed.
   */
  public interface RemoveListener extends RemoveListenerImpl<Anchor> {
  }
  
  interface RemoveListenerImpl<A> {
    void onAnchorRemoved(A anchor);
  }

  /**
   * Defines the behavior for this anchor if the text where it is anchored gets
   * deleted.
   */
  public enum RemovalStrategy {
    /**
     * Removes the anchor if the text is deleted. Read the {@link Anchor}
     * javadoc for caveats with line anchors.
     */
    REMOVE,

    /** Shifts the anchor's position if the text is deleted */
    SHIFT
  }

  /**
   * This id is guaranteed to have a lower insertion index than any other id
   * in the same column.
   */
  public static final int ID_FIRST_IN_COLUMN = -1;

  /**
   * Counter for  {@link #id} generation.
   *
   * <p>
   * <a href="http://code.google.com/webtoolkit/doc/latest/DevGuideCodingBasicsCompatibility.html">
   * Actually</a> it is not a 32-bit integer value, but 64-bit double value.
   * So it <a href="http://ecma262-5.com/ELS5_HTML.htm#Section_8.5">can</a>
   * address 2^53 positive integer values.
   */
  private static int nextId = 0;

  private boolean attached = true;

  private int column;

  private InsertionPlacementStrategy insertionPlacementStrategy =
      InsertionPlacementStrategy.DEFAULT;

  /**
   * The type of this anchor.
   */
  private final AnchorType type;

  private final int id;

  /**
   * The client may optionally stuff an opaque, dynamic value into the anchor
   */
  private Object value;

  private Line line;

  private int lineNumber;

  private ListenerManager<ShiftListenerImpl<? extends ReadOnlyAnchor>> shiftListenerManager;
  
  private ListenerManager<MoveListenerImpl<? extends ReadOnlyAnchor>> moveListenerManager;

  private ListenerManager<RemoveListenerImpl<? extends ReadOnlyAnchor>> removeListenerManager;

  private RemovalStrategy removalStrategy = RemovalStrategy.REMOVE;

  Anchor(AnchorType type, Line line, int lineNumber, int column) {
    this.type = type;
    this.id = nextId++;
    this.line = line;
    this.lineNumber = lineNumber;
    this.column = column;
  }

  @Override
  public int getColumn() {
    return column;
  }

  /**
   * @return a unique id that can serve as a stable identifier for this anchor.
   */
  @Override
  public int getId() {
    return id;
  }

  @Override
  public AnchorType getType() {
    return type;
  }

  /**
   * Update the value stored in this anchor
   *
   * @param value the opaque value to store
   */
  public void setValue(Object value) {
    this.value = value;
  }

  /**
   * @param <T>
   * @return the value stored in this anchor. Note that type safety is the
   *         responsibility of the caller.
   */
  @Override
  @SuppressWarnings("unchecked")
  public <T> T getValue() {
    return (T) value;
  }

  @Override
  public Line getLine() {
    return line;
  }

  public LineInfo getLineInfo() {
    return new LineInfo(line, lineNumber);
  }

  @Override
  public int getLineNumber() {
    return lineNumber;
  }

  @Override
  public boolean isLineAnchor() {
    return column == AnchorManager.IGNORE_COLUMN;
  }

  @Override
  public RemovalStrategy getRemovalStrategy() {
    return removalStrategy;
  }

  @Override
  public boolean hasLineNumber() {
    return lineNumber != AnchorManager.IGNORE_LINE_NUMBER;
  }

  @Override
  public boolean isAttached() {
    return attached;
  }

  @Override
  public InsertionPlacementStrategy getInsertionPlacementStrategy() {
    return insertionPlacementStrategy;
  }

  public void setInsertionPlacementStrategy(InsertionPlacementStrategy insertionPlacementStrategy) {
    this.insertionPlacementStrategy = insertionPlacementStrategy;
  }

  @SuppressWarnings("unchecked")
  @Override
  public ListenerRegistrar<ReadOnlyAnchor.ShiftListener> getReadOnlyShiftListenerRegistrar() {
    return (ListenerRegistrar) getShiftListenerRegistrar();
  }

  @SuppressWarnings("unchecked")
  public ListenerRegistrar<ShiftListener> getShiftListenerRegistrar() {
    if (shiftListenerManager == null) {
      shiftListenerManager = ListenerManager.create();
    }

    return (ListenerRegistrar) shiftListenerManager;
  }

  @SuppressWarnings("unchecked")
  @Override
  public ListenerRegistrar<ReadOnlyAnchor.MoveListener> getReadOnlyMoveListenerRegistrar() {
    return (ListenerRegistrar) getMoveListenerRegistrar();
  }

  @SuppressWarnings("unchecked")
  public ListenerRegistrar<MoveListener> getMoveListenerRegistrar() {
    if (moveListenerManager == null) {
      moveListenerManager = ListenerManager.create();
    }

    return (ListenerRegistrar) moveListenerManager;
  }

  @SuppressWarnings("unchecked")
  @Override
  public ListenerRegistrar<ReadOnlyAnchor.RemoveListener> getReadOnlyRemoveListenerRegistrar() {
    return (ListenerRegistrar) getRemoveListenerRegistrar();
  }

  @SuppressWarnings("unchecked")
  public ListenerRegistrar<RemoveListener> getRemoveListenerRegistrar() {
    if (removeListenerManager == null) {
      removeListenerManager = ListenerManager.create();
    }

    return (ListenerRegistrar) removeListenerManager;
  }

  public void setRemovalStrategy(RemovalStrategy removalStrategy) {
    this.removalStrategy = removalStrategy;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder(getType().toString());
    sb.append(":").append(getId());
    sb.append(" (").append(lineNumber).append(',').append(column).append(")");
    sb.append("[").append(value).append("]");
    sb.append(": ");

    String lineStr = line.toString();
    if (column <= lineStr.length()) {
      int split = Math.max(0, Math.min(column, lineStr.length()));

      sb.append(lineStr.subSequence(0, split));
      sb.append("^");
      sb.append(lineStr.substring(split));
    } else {
      sb.append(lineStr);
      for (int i = 0, n = column - lineStr.length(); i < n; i++) {
        sb.append(" ");
      }
      sb.append("^ (WARNING: the anchor is out-of-bounds)");
    }

    return sb.toString();
  }

  void dispatchShifted() {
    if (shiftListenerManager != null) {
      shiftListenerManager
          .dispatch(new Dispatcher<Anchor.ShiftListenerImpl<? extends ReadOnlyAnchor>>() {
            @SuppressWarnings("unchecked")
            @Override
            public void dispatch(ShiftListenerImpl listener) {
              listener.onAnchorShifted(Anchor.this);
            }
      });
    }
  }
  
  public void dispatchMoved() {
    if (moveListenerManager != null) {
      moveListenerManager
          .dispatch(new Dispatcher<Anchor.MoveListenerImpl<? extends ReadOnlyAnchor>>() {
            @SuppressWarnings("unchecked")
            @Override
            public void dispatch(MoveListenerImpl listener) {
              listener.onAnchorMoved(Anchor.this);
            }
      });
    }
  }
  
  void detach() {
    this.attached = false;
  }
  
  void dispatchRemoved() {
    if (removeListenerManager != null) {
      removeListenerManager
          .dispatch(new Dispatcher<Anchor.RemoveListenerImpl<? extends ReadOnlyAnchor>>() {
        @SuppressWarnings("unchecked")
        @Override
        public void dispatch(RemoveListenerImpl listener) {
          listener.onAnchorRemoved(Anchor.this);
        }
      });
    }
  }

  /**
   * Make sure all calls to this method are surrounded with removal and
   * re-addition to the list(s) it belongs in!
   */
  void setColumnWithoutDispatch(int column) {
    this.column = column;
  }

  void setLineWithoutDispatch(Line line, int lineNumber) {
    checkArgument(hasLineNumber() == (lineNumber != AnchorManager.IGNORE_LINE_NUMBER));
    this.line = line;
    this.lineNumber = lineNumber;
  }

  @Override
  public boolean hasColumn() {
    return column != AnchorManager.IGNORE_COLUMN;
  }
}
