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

import java.util.EnumSet;

import com.google.collide.client.editor.Buffer;
import com.google.collide.client.editor.FocusManager;
import com.google.collide.client.editor.Spacer;
import com.google.collide.client.editor.ViewportModel;
import com.google.collide.client.editor.ViewportModel.Edge;
import com.google.collide.client.editor.selection.SelectionModel;
import com.google.collide.client.util.ScheduledCommandExecutor;
import com.google.collide.client.util.logging.Log;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Document;
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.TextChange;
import com.google.collide.shared.document.util.LineUtils;
import com.google.collide.shared.document.util.LineUtils.LineVisitor;
import com.google.collide.shared.document.util.PositionUtils;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerRegistrar.Remover;

/*-
 * TODO:
 * - store enough info so we can safely skip the render step if
 * this was off-screen
 */
/**
 * A class to track changes in the document or editor state that result in a
 * render pass.
 */
class ChangeTracker
    implements
      Document.TextListener,
      ViewportModel.Listener,
      SelectionModel.SelectionListener,
      Buffer.SpacerListener,
      FocusManager.FocusListener {

  enum ChangeType {
    /** The viewport's top or bottom are now pointing to different lines */
    VIEWPORT_SHIFT,
    /** The viewport had a line added or removed */
    VIEWPORT_CONTENT,
    /** The contents of a line has changed */
    DIRTY_LINE,
    /** The selection has changed */
    SELECTION,
    /** The viewport's top or bottom line numbers have changed */
    VIEWPORT_LINE_NUMBER
  }

  private class RenderCommand extends ScheduledCommandExecutor {
    @Override
    protected void execute() {
      /*
       * TODO: think about whether a render pass can cause a
       * change, if so, need to fix some stuff here like clearing/cloning the
       * changes BEFORE calling out to the renderer
       */

      try {
        renderer.renderChanges();
      } catch (Throwable t) {
        Log.error(getClass(), t);
      }

      clearChangeState();
    }
  }

  /*
   * TODO: More cleanly group the variables that track changed
   * state
   */
  /** Tracks the types of changes that occurred */
  private final EnumSet<ChangeType> changes;
  /**
   * Tracks whether there was a content change that requires updating the top of
   * existing following lines
   */
  private boolean hadContentChangeThatRepositionsFollowingLines;
  /** List of lines that need to be re-rendered */
  private final JsonArray<Line> dirtyLines;

  private final LineUtils.LineVisitor dirtyMarkingLineVisitor = new LineVisitor() {
    @Override
    public boolean accept(Line line, int lineNumber, int beginColumn, int endColumn) {
      requestRenderLine(line);
      return true;
    }
  };

  private final Buffer buffer;
  private final JsonArray<Remover> listenerRemovers;
  /** Command that is scheduled-finally from any callback */
  private final RenderCommand renderCommand = new RenderCommand();
  private final Renderer renderer;
  private final SelectionModel selection;
  private final ViewportModel viewport;
  /**
   * List of lines that were removed. These were in the viewport at time of
   * removal (and hence were most likely rendered)
   */
  private final JsonArray<Line> viewportRemovedLines;
  /**
   * The line number of the topmost line that was added or removed, or
   * {@value Integer#MAX_VALUE} if there weren't any of these changes
   */
  private int topmostContentChangedLineNumber;

  private final EnumSet<ViewportModel.Edge> viewportLineNumberChangedEdges = EnumSet
      .noneOf(ViewportModel.Edge.class);
  
  ChangeTracker(Renderer renderer, Buffer buffer, Document document, ViewportModel viewport,
      SelectionModel selection, FocusManager focusManager) {
    this.buffer = buffer;
    this.renderer = renderer;
    this.selection = selection;
    this.listenerRemovers = JsonCollections.createArray();
    this.changes = EnumSet.noneOf(ChangeType.class);
    this.viewportRemovedLines = JsonCollections.createArray();
    this.dirtyLines = JsonCollections.createArray();
    this.viewport = viewport;

    attach(buffer, document, viewport, selection, focusManager);

    clearChangeState();
  }

  public EnumSet<ChangeType> getChanges() {
    return changes;
  }

  public JsonArray<Line> getDirtyLines() {
    return dirtyLines;
  }

  public JsonArray<Line> getViewportRemovedLines() {
    return viewportRemovedLines;
  }

  public int getTopmostContentChangedLineNumber() {
    return topmostContentChangedLineNumber;
  }

  /**
   * Returns whether the {@link ChangeType#VIEWPORT_CONTENT} change type was one
   * that requires updating the position of the
   * {@link #getTopmostContentChangedLineNumber()} and all following
   * lines.
   */
  public boolean hadContentChangeThatUpdatesFollowingLines() {
    return hadContentChangeThatRepositionsFollowingLines;
  }
  
  public EnumSet<ViewportModel.Edge> getViewportLineNumberChangedEdges() {
    return viewportLineNumberChangedEdges;
  }

  @Override
  public void onSelectionChange(Position[] oldSelectionRange, Position[] newSelectionRange) {
    /*
     * We only need to redraw those lines that either entered or left the
     * selection
     */
    Position[] viewportRange = getViewportRange();
    if (oldSelectionRange != null && newSelectionRange != null) {
      JsonArray<Position[]> differenceRanges =
          PositionUtils.getDifference(oldSelectionRange, newSelectionRange);
      for (int i = 0, n = differenceRanges.size(); i < n; i++) {
        markVisibleLinesDirty(differenceRanges.get(i), viewportRange);
      }
    } else if (oldSelectionRange != null) {
      markVisibleLinesDirty(oldSelectionRange, viewportRange);
    } else if (newSelectionRange != null) {
      markVisibleLinesDirty(newSelectionRange, viewportRange);
    }
  }

  private void markVisibleLinesDirty(Position[] dirtyRange, Position[] viewportRange) {
    /*
     * We can safely intersect with the viewport. The typical danger in doing
     * this right now is by the time things render, the viewport could have
     * shifted. But, in that case, the new viewport will have to be rendered
     * anyway, and thus the selection in that new viewport would be drawn
     * regardless of any dirty-marking we do here.
     */
    Position[] visibleRange = PositionUtils.getIntersection(dirtyRange, viewportRange);
    if (visibleRange != null) {
      PositionUtils.visit(dirtyMarkingLineVisitor, visibleRange[0], visibleRange[1]);
    }
  }

  private Position[] getViewportRange() {
    return new Position[] {new Position(viewport.getTop(), 0),
        new Position(viewport.getBottom(), viewport.getBottomLine().getText().length() - 1)};
  }

  @Override
  public void onTextChange(Document document, JsonArray<TextChange> textChanges) {

    for (int i = 0, n = textChanges.size(); i < n; i++) {
      /*
       * For insertion, the second line through the second-to-last line can't
       * have existed in the document, so no point in marking them dirty.
       */
      TextChange textChange = textChanges.get(i);

      Line line = textChange.getLine();
      Line lastLine = textChange.getLastLine();

      if (dirtyLines.indexOf(line) == -1) {
        dirtyLines.add(line);
      }

      if (line != lastLine && dirtyLines.indexOf(lastLine) == -1) {
        dirtyLines.add(lastLine);
      }
    }

    scheduleRender(ChangeType.DIRTY_LINE);
  }

  @Override
  public void onViewportContentChanged(ViewportModel viewport, int lineNumber,
      boolean added, JsonArray<Line> lines) {
    int relevantContentChangedLineNumber;
    if (!added && viewport.getTopLineNumber() == lineNumber - 1) {
      // TODO: rework this case is handled naturally
      /*
       * This handles the top viewport line being "removed" by backspacing on
       * column 0. In this case, no one else draws the new top line of the
       * viewport (the one that got the previous top line's contents appended to
       * it).
       */
      relevantContentChangedLineNumber = viewport.getTopLineNumber();
    } else {
      relevantContentChangedLineNumber = lineNumber;
    }

    if (!added) {
      for (int i = 0, n = lines.size(); i < n; i++) {
        Line curLine = lines.get(i);
        if (ViewportRenderer.isRendered(curLine)) {
          viewportRemovedLines.add(curLine);
        }
      }
    }

    /*
     * If there is a spacer in the viewport below the line number change, line numbers shift
     * non-uniformly around it.
     */
    /*
     * TODO: actually implement the check for spacers in the viewport, not just
     * the document.
     */
    handleContentChange(relevantContentChangedLineNumber, buffer.hasSpacers());
  }

  @Override
  public void onViewportLineNumberChanged(ViewportModel viewport, Edge edge) {
    viewportLineNumberChangedEdges.add(edge);
    scheduleRender(ChangeType.VIEWPORT_LINE_NUMBER);
  }

  @Override
  public void onViewportShifted(ViewportModel viewport, LineInfo top, LineInfo bottom,
      LineInfo oldTop, LineInfo oldBottom) {
    scheduleRender(ChangeType.VIEWPORT_SHIFT);
  }

  @Override
  public void onFocusChange(boolean hasFocus) {
    /*
     * Schedule re-rendering of the lines in the selection so that we can update
     * the selection color based on focused state. Note that by the time we are
     * rendering, the selection could have changed. This is OK since in that
     * case, the new selection has to be rendered anyway, and it will render in
     * the correct color.
     */
    if (selection.hasSelection()) {
      markVisibleLinesDirty(selection.getSelectionRange(true), getViewportRange());
    }
  }

  public void requestRenderLine(Line line) {
    if (dirtyLines.indexOf(line) == -1) {
      dirtyLines.add(line);
    }

    scheduleRender(ChangeType.DIRTY_LINE);
  }

  void teardown() {
    for (int i = 0, n = listenerRemovers.size(); i < n; i++) {
      listenerRemovers.get(i).remove();
    }

    renderCommand.cancel();
  }

  private void attach(Buffer buffer, Document document, ViewportModel viewport,
      SelectionModel selection, FocusManager focusManager) {
    listenerRemovers.add(focusManager.getFocusListenerRegistrar().add(this));
    listenerRemovers.add(document.getTextListenerRegistrar().add(this));
    listenerRemovers.add(viewport.getListenerRegistrar().add(this));
    listenerRemovers.add(selection.getSelectionListenerRegistrar().add(this));
    listenerRemovers.add(buffer.getSpacerListenerRegistrar().add(this));
  }

  private void clearChangeState() {
    changes.clear();
    viewportRemovedLines.clear();
    dirtyLines.clear();
    topmostContentChangedLineNumber = Integer.MAX_VALUE;
    hadContentChangeThatRepositionsFollowingLines = false;
    viewportLineNumberChangedEdges.clear();
  }

  @Override
  public void onSpacerAdded(Spacer spacer) {
    handleContentChange(spacer.getLineNumber(), true);
  }

  @Override
  public void onSpacerRemoved(Spacer spacer, Line oldLine, int oldLineNumber) {
    handleContentChange(oldLineNumber, true);
  }

  @Override
  public void onSpacerHeightChanged(Spacer spacer, int oldHeight) {
    handleContentChange(spacer.getLineNumber(), true);
  }

  private void handleContentChange(int lineNumber, boolean requiresRepositioningFollowingLines) {
    hadContentChangeThatRepositionsFollowingLines |= requiresRepositioningFollowingLines;
    if (topmostContentChangedLineNumber > lineNumber) {
      topmostContentChangedLineNumber = lineNumber;
    }
    scheduleRender(ChangeType.VIEWPORT_CONTENT);
  }

  private void scheduleRender(ChangeType change) {
    changes.add(change);
    renderCommand.scheduleFinally();
  }
}
