// 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.editor.selection;

import static com.google.collide.shared.document.util.LineUtils.getLastCursorColumn;
import static com.google.collide.shared.document.util.LineUtils.rubberbandColumn;

import org.waveprotocol.wave.client.common.util.UserAgent;

import com.google.collide.client.document.linedimensions.LineDimensionsCalculator.RoundingStrategy;
import com.google.collide.client.editor.Buffer;
import com.google.collide.client.editor.ViewportModel;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.DocumentMutator;
import com.google.collide.shared.document.Line;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.Position;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorType;
import com.google.collide.shared.document.anchor.AnchorUtils;
import com.google.collide.shared.document.anchor.InsertionPlacementStrategy;
import com.google.collide.shared.document.anchor.ReadOnlyAnchor;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.document.util.PositionUtils;
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;
import com.google.collide.shared.util.TextUtils;
import com.google.collide.shared.util.UnicodeUtils;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
import com.google.gwt.regexp.shared.RegExp;

// TODO: this class is getting huge, time to split responsibilities
/**
 * A class that models the user's selection. In addition to storing the
 * selection and cursor positions, this class listens for mouse drags and other
 * actions that affect the selection.
 *
 * The lifecycle of this class is tied to the current document. When the
 * document is replaced, a new instance of this class is created for the new
 * document.
 */
public class SelectionModel implements Buffer.MouseDragListener {

  /**
   * Enumeration of movement actions.
   */
  public enum MoveAction {
    LEFT,
    RIGHT,
    WORD_LEFT,
    WORD_RIGHT,
    UP,
    DOWN,
    PAGE_UP,
    PAGE_DOWN,
    LINE_START,
    LINE_END,
    TEXT_START,
    TEXT_END
  }

  private static final AnchorType SELECTION_ANCHOR_TYPE = AnchorType.create(SelectionModel.class,
      "selection");

  /**
   * Listener that is called when the user's cursor changes position.
   */
  public interface CursorListener {
    /**
     * @param isExplicitChange true if this change was a result of either the
     *        user moving his cursor or through programatic setting, or false if
     *        it was caused by text mutations in the document
     */
    void onCursorChange(LineInfo lineInfo, int column, boolean isExplicitChange);
  }

  /**
   * Listener that is called when the user changes his selection. This will not
   * be called if the selection's position in the document shifts
   * because of edits elsewhere in the document.
   *
   * Note: The selection is different from the cursor. This will not be called
   * if the user does not have a selection and his cursor moves.
   */
  public interface SelectionListener {
    /**
     * @param oldSelectionRange the selection range before this selection, or
     *        null if there was not a selection
     * @param newSelectionRange the new selection range, or null if there is not
     *        a selection
     */
    void onSelectionChange(Position[] oldSelectionRange, Position[] newSelectionRange);
  }

  private class AnchorListener implements Anchor.ShiftListener {
    @Override
    public void onAnchorShifted(Anchor anchor) {
      if (anchor == cursorAnchor) {
        preferredCursorColumn = anchor.getColumn();
      }

      dispatchCursorChange(false);
    }
  }

  /**
   * A repeating command that continues a user's drag-based selection when the
   * user's mouse pointer moves outside of the editor.
   */
  // TODO: split out MouseDragRepeater into a smaller class
  private class MouseDragRepeater implements RepeatingCommand {
    private static final int REPEAT_PERIOD_MS = 100;

    private int deltaX;
    private int deltaY;

    @Override
    public boolean execute() {
      // check for movement this frame
      if (deltaY == 0 && deltaX == 0) {
        return false;
      }

      LineInfo cursorLineInfo = cursorAnchor.getLineInfo();
      int cursorColumn = cursorAnchor.getColumn();
      int newScrollTop = buffer.getScrollTop() + deltaY;

      if (deltaY != 0) {
        int targetCursorY = deltaY < 0 ? newScrollTop : newScrollTop + buffer.getHeight();
        int cursorLineNumber = buffer.convertYToLineNumber(targetCursorY, true);
        int actualCursorTop = buffer.convertLineNumberToY(cursorLineNumber);
        if (deltaY < 0 && actualCursorTop < newScrollTop && cursorLineNumber > 0) {
          /*
           * The current line is partially visible, increment so we get a fully
           * visible line
           */
          cursorLineNumber++;
        } else if (deltaY > 0 && cursorLineNumber < document.getLastLineNumber()) {
          // See above
          cursorLineNumber--;
        }

        cursorLineInfo = document.getLineFinder().findLine(cursorLineNumber);
      }

      if (deltaX != 0) {
        int targetCursorX =
            buffer.calculateColumnLeft(cursorLineInfo.line(), cursorAnchor.getColumn()) + deltaX;
        cursorColumn = buffer.convertXToRoundedVisibleColumn(targetCursorX, cursorLineInfo.line());
      }

      buffer.setScrollTop(newScrollTop);
      if (viewport.isLineNumberFullyVisibleInViewport(cursorLineInfo.number())) {
        // Only move cursor if the target line is visible inside of viewport
        moveCursorUsingSelectionGranularity(
            cursorLineInfo, buffer.convertColumnToX(cursorLineInfo.line(), cursorColumn), false);
      }

      return true;
    }

    private void schedule(int deltaX, int deltaY) {
      if (this.deltaX == 0 && this.deltaY == 0) {
        // The repeated command is not scheduled, so schedule it
        Scheduler.get().scheduleFixedPeriod(this, REPEAT_PERIOD_MS);
      }

      this.deltaX = deltaX;
      this.deltaY = deltaY;
    }

    private void cancel() {
      deltaX = 0;
      deltaY = 0;
    }
  }

  private enum SelectionGranularity {
    CHARACTER, WORD, LINE;

    private static SelectionGranularity forClickCount(int clickCount) {
      switch (clickCount) {
        case 1:
          return CHARACTER;
        case 2:
          return WORD;
        case 3:
          return LINE;
        default:
          return CHARACTER;
      }
    }
  }

  public static SelectionModel create(Document document, Buffer buffer) {
    ListenerRegistrar.RemoverManager removalManager = new ListenerRegistrar.RemoverManager();
    SelectionModel selection = new SelectionModel(document, buffer, removalManager);
    removalManager.track(buffer.getMouseDragListenerRegistrar().add(selection));

    return selection;
  }

  private Anchor createSelectionAnchor(Line line, int lineNumber, int column, Document document,
      AnchorListener anchorListener) {
    Anchor anchor =
        document.getAnchorManager().createAnchor(SELECTION_ANCHOR_TYPE, line, lineNumber, column);
    anchor.setRemovalStrategy(Anchor.RemovalStrategy.SHIFT);
    anchor.getShiftListenerRegistrar().add(anchorListener);
    return anchor;
  }

  private final AnchorListener anchorListener;

  /**
   * The anchor of the selection ("anchor" defined as "where the selection
   * began", not "anchor" defined in terms of document anchors).
   */
  private final Anchor baseAnchor;
  private final Buffer buffer;

  /** The cursor of the selection */
  private final Anchor cursorAnchor;
  private final ListenerManager<CursorListener> cursorListenerManager;
  private final Document document;
  /**
   * While the user is dragging, this defines the lower bound for the minimum
   * selection that must be selected regardless of where the user's mouse
   * pointer is. This should be null outside of a drag.
   *
   * For example, if the user is in word-selection mode (by double-clicking to
   * start the selection), the minimum selection will be the initial word that
   * was double-clicked.
   */
  private Anchor minimumDragSelectionLowerBound;
  /** Like {@link #minimumDragSelectionLowerBound}, this defines the upper bound */
  private Anchor minimumDragSelectionUpperBound;
  private final MouseDragRepeater mouseDragRepeater = new MouseDragRepeater();

  /**
   * Tracks the column that the user explicitly moved to. For example, the user
   * moves to line 2, column 80 and then presses the up arrow. Line 1 only has
   * 30 columns, so it will move to column 30, but this will still be column 80
   * so if the user presses the down arrow, it will take him back to column 80.
   */
  private int preferredCursorColumn;
  private SelectionGranularity selectionGranularity = SelectionGranularity.CHARACTER;
  private final ListenerManager<SelectionListener> selectionListenerManager;
  private final ListenerRegistrar.RemoverManager removerManager;

  private ViewportModel viewport;

  private SelectionModel(
      Document document, Buffer buffer, ListenerRegistrar.RemoverManager removerManager) {
    this.document = document;
    this.buffer = buffer;
    this.removerManager = removerManager;
    anchorListener = new AnchorListener();
    cursorAnchor = createSelectionAnchor(document.getFirstLine(), 0, 0, document, anchorListener);
    baseAnchor = createSelectionAnchor(document.getFirstLine(), 0, 0, document, anchorListener);
    cursorListenerManager = ListenerManager.create();
    selectionListenerManager = ListenerManager.create();
  }

  public void deleteSelection(DocumentMutator documentMutator) {
    Preconditions.checkState(hasSelection(), "can't delete selection when there is no selection");
    Position[] selectionRange = getSelectionRange(true);
    /*
     * TODO: optimize. It's currently O(n) where n is the number of
     * lines, but can be O(1) with an additional delete API
     */
    int deleteCount =
        LineUtils.getTextCount(selectionRange[0].getLine(), selectionRange[0].getColumn(),
            selectionRange[1].getLine(), selectionRange[1].getColumn());
    documentMutator.deleteText(selectionRange[0].getLine(), selectionRange[0].getLineNumber(),
        selectionRange[0].getColumn(), deleteCount);
  }

  public void deselect() {
    if (!hasSelection()) {
      return;
    }

    Position[] oldSelectionRange = getSelectionRangeForCallback();
    moveAnchor(baseAnchor, cursorAnchor.getLineInfo(), cursorAnchor.getColumn(), false);
    dispatchSelectionChange(oldSelectionRange);
  }

  public int getBaseColumn() {
    return baseAnchor.getColumn();
  }

  public Line getBaseLine() {
    return baseAnchor.getLine();
  }

  public int getBaseLineNumber() {
    return baseAnchor.getLineNumber();
  }

  public int getCursorColumn() {
    return cursorAnchor.getColumn();
  }

  public Line getCursorLine() {
    return cursorAnchor.getLine();
  }

  public int getCursorLineNumber() {
    return cursorAnchor.getLineNumber();
  }

  public ListenerRegistrar<CursorListener> getCursorListenerRegistrar() {
    return cursorListenerManager;
  }

  public ListenerRegistrar<SelectionListener> getSelectionListenerRegistrar() {
    return selectionListenerManager;
  }

  // TODO: I think we should introduce SelectionRange bean.
  /**
   * Returns the selection range where position[0] is always the logical start
   * of selection and position[1] is always the logical end.
   *
   * @param inclusiveEnd true for the returned position[1] to be the last
   *        character in the selection, false for position[1] to be the
   *        character after the last character in the selection. If true there
   *        must currently be a selection.
   */
  public Position[] getSelectionRange(boolean inclusiveEnd) {
    Preconditions.checkArgument(
        hasSelection() || !inclusiveEnd, "There must be a selection if inclusiveEnd is requested.");
    Position[] selection = new Position[2];

    Anchor beginAnchor = getEarlierSelectionAnchor();
    Anchor endAnchor = getLaterSelectionAnchor();

    selection[0] = new Position(beginAnchor.getLineInfo(), beginAnchor.getColumn());

    if (inclusiveEnd) {
      Preconditions.checkState(hasSelection(),
          "Can't get selection range inclusive end when nothing is selected");
      selection[1] =
          PositionUtils.getPosition(endAnchor.getLine(), endAnchor.getLineNumber(),
              endAnchor.getColumn(), -1);
    } else {
      selection[1] = new Position(endAnchor.getLineInfo(), endAnchor.getColumn());
    }

    return selection;
  }

  public int getSelectionBeginLineNumber() {
    return isCursorAtEndOfSelection() ? baseAnchor.getLineNumber() : cursorAnchor.getLineNumber();
  }

  public int getSelectionEndLineNumber() {
    return isCursorAtEndOfSelection() ? cursorAnchor.getLineNumber()
        : baseAnchor.getLineNumber();
  }

  public boolean hasSelection() {
    return AnchorUtils.compare(cursorAnchor, baseAnchor) != 0;
  }

  public String getSelectedText() {
    if (!hasSelection()) {
      return "";
    }

    Position[] selectionRange = getSelectionRange(true);
    return LineUtils.getText(selectionRange[0].getLine(), selectionRange[0].getColumn(),
        selectionRange[1].getLine(), selectionRange[1].getColumn());
  }

  /**
   * Returns true if the selection spans a newline character.
   */
  public boolean hasMultilineSelection() {
    return cursorAnchor.getLine() != baseAnchor.getLine();
  }

  public boolean isCursorAtEndOfSelection() {
    return AnchorUtils.compare(cursorAnchor, baseAnchor) >= 0;
  }

  /**
   * Performs specified movement action.
   */
  public void move(MoveAction action, boolean isShiftHeld) {
    boolean shouldUpdatePreferredColumn = true;
    int column = cursorAnchor.getColumn();
    LineInfo lineInfo = cursorAnchor.getLineInfo();
    String lineText = lineInfo.line().getText();

    switch (action) {
      case LEFT:
        column = TextUtils.findPreviousNonMarkNorOtherCharacter(lineText, column);
        break;

      case RIGHT:
        column = TextUtils.findNonMarkNorOtherCharacter(lineText, column);
        break;

      case WORD_LEFT:
        column = TextUtils.findPreviousWord(lineText, column, false);
        /**
         * {@link TextUtils#findNextWord} can return line length indicating it's
         * at the end of a word on the line. If this line ends in a* {@code \n}
         * that will cause us to move to the next line when we check
         * {@link LineUtils#getLastCursorColumn} which isn't what we want. So
         * fix it now in case the lines ends in {@code \n}.
         */
        if (column == lineInfo.line().length()) {
          column = rubberbandColumn(lineInfo.line(), column);
        }
        break;

      case WORD_RIGHT:
        column = TextUtils.findNextWord(lineText, column, true);
        /**
         * {@link TextUtils#findNextWord} can return line length indicating it's
         * at the end of a word on the line. If this line ends in a* {@code \n}
         * that will cause us to move to the next line when we check
         * {@link LineUtils#getLastCursorColumn} which isn't what we want. So
         * fix it now in case the lines ends in {@code \n}.
         */
        if (column == lineInfo.line().length()) {
          column = rubberbandColumn(lineInfo.line(), column);
        }
        break;

      case UP:
        column = preferredCursorColumn;
        if (lineInfo.line() == document.getFirstLine() && (isShiftHeld || UserAgent.isMac())) {
          /*
           * Pressing up on the first line should:
           * - On Mac, always go to first column, or
           * - On all platforms, shift+up should select to first column
           */
          column = 0;
        } else {
          lineInfo.moveToPrevious();
        }

        column = rubberbandColumn(lineInfo.line(), column);
        shouldUpdatePreferredColumn = false;
        break;

      case DOWN:
        column = preferredCursorColumn;
        if (lineInfo.line() == document.getLastLine() && (isShiftHeld || UserAgent.isMac())) {
          // Consistent with up-arrowing on first line
          column = LineUtils.getLastCursorColumn(lineInfo.line());
        } else {
          lineInfo.moveToNext();
        }

        column = rubberbandColumn(lineInfo.line(), column);
        shouldUpdatePreferredColumn = false;
        break;

      case PAGE_UP:
        for (int i = buffer.getFlooredHeightInLines(); i > 0; i--) {
          lineInfo.moveToPrevious();
        }
        column = rubberbandColumn(lineInfo.line(), preferredCursorColumn);
        shouldUpdatePreferredColumn = false;
        break;

      case PAGE_DOWN:
        for (int i = buffer.getFlooredHeightInLines(); i > 0; i--) {
          lineInfo.moveToNext();
        }
        column = rubberbandColumn(lineInfo.line(), preferredCursorColumn);
        shouldUpdatePreferredColumn = false;
        break;

      case LINE_START:
        int firstNonWhitespaceColumn = TextUtils.countWhitespacesAtTheBeginningOfLine(
            lineInfo.line().getText());
        column = (column != firstNonWhitespaceColumn) ? firstNonWhitespaceColumn : 0;
        break;

      case LINE_END:
        column = LineUtils.getLastCursorColumn(lineInfo.line());
        break;

      case TEXT_START:
        lineInfo = new LineInfo(document.getFirstLine(), 0);
        column = 0;
        break;

      case TEXT_END:
        lineInfo = new LineInfo(document.getLastLine(), document.getLineCount() - 1);
        column = LineUtils.getLastCursorColumn(lineInfo.line());
        break;
    }

    if (column < 0) {
      if (lineInfo.moveToPrevious()) {
        column = getLastCursorColumn(lineInfo.line());
      } else {
        column = 0;
      }
    } else if (column > getLastCursorColumn(lineInfo.line())) {
      if (lineInfo.moveToNext()) {
        column = LineUtils.getFirstCursorColumn(lineInfo.line());
      } else {
        column = rubberbandColumn(lineInfo.line(), column);
      }
    }

    moveCursor(lineInfo, column, shouldUpdatePreferredColumn, isShiftHeld,
        getSelectionRangeForCallback());
  }

  @Override
  public void onMouseClick(Buffer buffer, int clickCount, int x, int y, boolean isShiftHeld) {
    int lineNumber = buffer.convertYToLineNumber(y, true);
    LineInfo newLineInfo =
        buffer.getDocument().getLineFinder().findLine(cursorAnchor.getLineInfo(), lineNumber);
    int newColumn = buffer.convertXToRoundedVisibleColumn(x, newLineInfo.line());
    // Allow the user to keep clicking to iterate through selection modes
    clickCount = (clickCount - 1) % 3 + 1;

    selectionGranularity = SelectionGranularity.forClickCount(clickCount);

    if (clickCount == 1) {
      moveCursor(newLineInfo, newColumn, true, isShiftHeld, getSelectionRangeForCallback());
    } else {
      setInitialSelectionForGranularity(newLineInfo, newColumn, x);
    }
  }

  private void setInitialSelectionForGranularity(LineInfo lineInfo, int column, int x) {

    /*
     * If the given column is more the line's length (for example, when appending to the last line
     * of the doc), then just assume no initial selection (since most of that calculation code
     * relies on getting the out-of-bounds character).
     */
    int lineTextLength = lineInfo.line().getText().length();
    if (column >= lineTextLength) {
      moveCursor(lineInfo, lineTextLength, true, false, getSelectionRangeForCallback());
    } else if (selectionGranularity == SelectionGranularity.WORD) {
      Line line = lineInfo.line();
      String text = line.getText();
      if (UnicodeUtils.isWhitespace(text.charAt(column))) {
        moveCursor(lineInfo, column, true, false, getSelectionRangeForCallback());
      } else {
        // Start seeking from the next column so the character under cursor
        // will belong to the "previous word".
        int nextColumn = column + 1;
        int wordStartColumn = TextUtils.findPreviousWord(text, nextColumn, false);
        wordStartColumn = LineUtils.rubberbandColumn(line, wordStartColumn);
        moveAnchor(baseAnchor, lineInfo, wordStartColumn, false);
        moveCursorUsingSelectionGranularity(lineInfo, x, false);
      }
    } else if (selectionGranularity == SelectionGranularity.LINE) {
      moveAnchor(baseAnchor, lineInfo, 0, false);
      moveCursorUsingSelectionGranularity(lineInfo, x, false);
    }
  }

  @Override
  public void onMouseDrag(Buffer buffer, int x, int y) {
    /*
     * The click callback sets up the initial selection, this will become the
     * minimum selection
     */
    ensureMinimumDragSelectionFromCurrentSelection();

    int lineNumber = buffer.convertYToLineNumber(y, true);
    LineInfo newLineInfo =
        document.getLineFinder().findLine(cursorAnchor.getLineInfo(), lineNumber);

    // Only move the cursor
    if (viewport.isLineNumberFullyVisibleInViewport(newLineInfo.number())) {
      moveCursorUsingSelectionGranularity(newLineInfo, x, false);
    }
    manageRepeaterForDrag(x, y);
  }

  private void ensureMinimumDragSelectionFromCurrentSelection() {
    if (minimumDragSelectionLowerBound != null) {
      return;
    }

    Position[] selectionRange = getSelectionRange(false);
    minimumDragSelectionLowerBound = createAnchorFromPosition(selectionRange[0]);
    minimumDragSelectionUpperBound = createAnchorFromPosition(selectionRange[1]);
  }

  private Anchor createAnchorFromPosition(Position position) {
    return document.getAnchorManager().createAnchor(SELECTION_ANCHOR_TYPE, position.getLine(),
        position.getLineInfo().number(), position.getColumn());
  }

  private void removeMinimumDragSelection() {
    if (minimumDragSelectionLowerBound == null) {
      return;
    }

    document.getAnchorManager().removeAnchor(minimumDragSelectionLowerBound);
    document.getAnchorManager().removeAnchor(minimumDragSelectionUpperBound);
    minimumDragSelectionLowerBound = minimumDragSelectionUpperBound = null;
  }

  /**
   * Moves the cursor in the general direction of the {@code targetColumn}, but
   * since this takes into account the {@link #selectionGranularity}, the actual
   * column may be different.
   *
   * @param targetLineInfo the cursor will (mostly) stay within this line. Most
   *        callers will give the line underneath the mouse pointer as this
   *        parameter. (The cursor may move to the next line if the selection
   *        granularity is line.)
   */
  @SuppressWarnings("incomplete-switch")
  private void moveCursorUsingSelectionGranularity(LineInfo targetLineInfo, int x,
      boolean updatePreferredColumn) {

    Line targetLine = targetLineInfo.line();
    int roundedTargetColumn = buffer.convertXToRoundedVisibleColumn(x, targetLine);
    // Forward if the cursor anchor will be ahead of the base anchor
    boolean growForward =
        AnchorUtils.compare(baseAnchor, targetLineInfo.number(), roundedTargetColumn) <= 0;

    LineInfo newLineInfo = targetLineInfo;
    int newColumn = roundedTargetColumn;

    switch (selectionGranularity) {
      case WORD:
        if (growForward) {
          /*
           * Floor the column so the last pixel of the last character of the
           * current word does not trigger a finding of the next word
           */
          newColumn =
              TextUtils.findNextWord(
                  targetLine.getText(),
                  buffer.convertXToColumn(x, targetLine, RoundingStrategy.FLOOR), false);
        } else {
          // See note above about flooring, but we ceil here instead
          newColumn =
              TextUtils.findPreviousWord(
                  targetLine.getText(),
                  buffer.convertXToColumn(x, targetLine, RoundingStrategy.CEIL), false);
        }
        break;

      case LINE:
        // The cursor is on column 0 regardless
        newColumn = 0;
        if (growForward) {
          // If growing forward, move to the next line, if possible
          newLineInfo = targetLineInfo.copy();
          if (!newLineInfo.moveToNext()) {
            /*
             * There isn't a next line, so just move the cursor to the end of
             * line
             */
            newColumn = LineUtils.getLastCursorColumn(newLineInfo.line());
          }
        }
        break;
    }

    Position[] oldSelectionRange = getSelectionRangeForCallback();

    newColumn = LineUtils.rubberbandColumn(newLineInfo.line(), newColumn);
    ensureNewSelectionObeysMinimumDragSelection(newLineInfo, newColumn);

    moveCursor(newLineInfo, newColumn, updatePreferredColumn, true, oldSelectionRange);
  }

  private void ensureNewSelectionObeysMinimumDragSelection(LineInfo newCursorLineInfo,
      int newCursorColumn) {

    if (minimumDragSelectionLowerBound == null
        || AnchorUtils.compare(minimumDragSelectionLowerBound,
            minimumDragSelectionUpperBound) == 0) {
      // There isn't a minimum drag selection set
      return;
    }

    // Is the new selection growing forward?
    boolean newGrowForward =
        AnchorUtils.compare(baseAnchor, newCursorLineInfo.number(), newCursorColumn) <= 0;

    boolean newSelectionIsAheadOfMinimum =
        newGrowForward && AnchorUtils.compare(baseAnchor, minimumDragSelectionUpperBound) >= 0;
    boolean newSelectionIsBehindMinimum =
        !newGrowForward && AnchorUtils.compare(baseAnchor, minimumDragSelectionLowerBound) <= 0;

    // Move base anchor to correct minimum selection bound
    Anchor newBaseAnchorPosition = null;
    if (newSelectionIsBehindMinimum) {
      newBaseAnchorPosition = minimumDragSelectionUpperBound;
    } else if (newSelectionIsAheadOfMinimum) {
      newBaseAnchorPosition = minimumDragSelectionLowerBound;
    }

    if (newBaseAnchorPosition != null) {
      moveAnchor(baseAnchor, newBaseAnchorPosition.getLineInfo(),
          newBaseAnchorPosition.getColumn(), false);
    }
  }

  private void manageRepeaterForDrag(int x, int y) {
    int bufferScrollLeft = buffer.getScrollLeft();
    int bufferScrollTop = buffer.getScrollTop();
    int bufferHeight = buffer.getHeight();
    int bufferWidth = buffer.getWidth();

    int deltaX = 0;
    int deltaY = 0;

    if (y - bufferScrollTop < 0) {
      deltaY = y - bufferScrollTop;
    } else if (y >= bufferScrollTop + bufferHeight) {
      deltaY = y - (bufferScrollTop + bufferHeight);
    }

    if (x - bufferScrollLeft < 0) {
      deltaX = x - bufferScrollLeft;
    } else if (x >= bufferScrollLeft + bufferWidth) {
      deltaX = x - (bufferScrollLeft + bufferWidth);
    }

    if (deltaX == 0 && deltaY == 0) {
      mouseDragRepeater.cancel();
    } else {
      mouseDragRepeater.schedule(deltaX, deltaY);
    }
  }

  @Override
  public void onMouseDragRelease(Buffer buffer, int x, int y) {
    mouseDragRepeater.cancel();
    removeMinimumDragSelection();
  }

  public void setSelection(LineInfo baseLineInfo, int baseColumn, LineInfo cursorLineInfo,
      int cursorColumn) {

    Preconditions.checkArgument(baseColumn <= LineUtils.getLastCursorColumn(baseLineInfo.line()),
        "The base column is out-of-bounds");
    int lastCursorColumn = LineUtils.getLastCursorColumn(cursorLineInfo.line());
    Preconditions.checkArgument(cursorColumn <= lastCursorColumn,
        "The cursor column is out-of-bounds. Expected <= " + lastCursorColumn
            + ", got " + cursorColumn + ", line " + cursorLineInfo.number());

    baseColumn = LineUtils.rubberbandColumn(baseLineInfo.line(), baseColumn);
    cursorColumn = LineUtils.rubberbandColumn(cursorLineInfo.line(), cursorColumn);

    Position[] oldSelectionRange = getSelectionRangeForCallback();

    moveAnchor(baseAnchor, baseLineInfo, baseColumn, false);
    boolean hasSelection =
        LineUtils.comparePositions(cursorLineInfo.number(), cursorColumn, baseLineInfo.number(),
            baseColumn) != 0;
    moveCursor(cursorLineInfo, cursorColumn, true, hasSelection, oldSelectionRange);
  }

  public void setCursorPosition(LineInfo lineInfo, int column) {
    int lastCursorColumn = LineUtils.getLastCursorColumn(lineInfo.line());
    Preconditions.checkArgument(column <= lastCursorColumn,
        "The cursor column is out-of-bounds. Expected <= " + lastCursorColumn
            + ", got " + column + ", line " + lineInfo.number());
    moveCursor(lineInfo, column, true, hasSelection(), getSelectionRangeForCallback());
  }

  public void selectAll() {
    Position[] oldSelectionRange = getSelectionRangeForCallback();

    moveAnchor(baseAnchor, new LineInfo(document.getFirstLine(), 0), 0, false);
    moveCursor(new LineInfo(document.getLastLine(), document.getLastLineNumber()),
        LineUtils.getLastCursorColumn(document.getLastLine()), true, true, oldSelectionRange);
  }

  public void teardown() {
    removerManager.remove();
    if (baseAnchor != cursorAnchor) {
      document.getAnchorManager().removeAnchor(baseAnchor);
    }
  }

  public ReadOnlyAnchor getCursorAnchor() {
    return cursorAnchor;
  }

  public Position getCursorPosition() {
    return new Position(cursorAnchor.getLineInfo(), cursorAnchor.getColumn());
  }

  private void dispatchCursorChange(final boolean isExplicitChange) {
    cursorListenerManager.dispatch(new Dispatcher<SelectionModel.CursorListener>() {
      @Override
      public void dispatch(CursorListener listener) {
        listener.onCursorChange(cursorAnchor.getLineInfo(), cursorAnchor.getColumn(),
            isExplicitChange);
      }
    });
  }

  private void dispatchSelectionChange(final Position[] oldSelectionRange) {
    selectionListenerManager.dispatch(new Dispatcher<SelectionModel.SelectionListener>() {
      @Override
      public void dispatch(SelectionListener listener) {
        listener.onSelectionChange(oldSelectionRange, getSelectionRangeForCallback());
      }
    });
  }

  private Position[] getSelectionRangeForCallback() {
    return hasSelection() ? getSelectionRange(true) : null;
  }

  /**
   * Moves the cursor and potentially the base. This method will dispatch the
   * appropriate callbacks.
   *
   * @param lineInfo the line where the cursor will be positioned
   * @param column the column (on the given line) where the cursor will be
   *        positioned
   * @param updatePreferredColumn see {@link #preferredCursorColumn}
   * @param isSelecting false to ensure there is not a selection after the
   *        movement
   * @param oldSelectionRange the selection range (via
   *        {@link #getSelectionRangeForCallback()}) before the caller modified
   *        the selection. This will be passed to the selection callback as the
   *        old selection range.
   */
  private void moveCursor(LineInfo lineInfo, int column, boolean updatePreferredColumn,
      boolean isSelecting, Position[] oldSelectionRange) {

    boolean hadSelection = hasSelection();

    // Check if base anchor should move
    if (!isSelecting) {
      moveAnchor(baseAnchor, lineInfo, column, false);
    }

    // Move cursor anchor
    moveAnchor(cursorAnchor, lineInfo, column, updatePreferredColumn);

    boolean willHaveSelection = hasSelection();

    dispatchCursorChange(true);
    if (isSelecting || willHaveSelection != hadSelection) {
      dispatchSelectionChange(oldSelectionRange);
    }
  }

  private void moveAnchor(Anchor anchor, LineInfo lineInfo, int column,
      boolean updatePreferredColumn) {

    if (anchor.getLine().equals(lineInfo.line()) && anchor.getColumn() == column) {
      return;
    }

    if (updatePreferredColumn) {
      preferredCursorColumn = column;
    }

    document.getAnchorManager().moveAnchor(anchor, lineInfo.line(), lineInfo.number(), column);
  }

  public void initialize(ViewportModel viewport) {
    this.viewport = viewport;
  }

  /**
