// 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 collide.client.util.Elements;

import com.google.collide.client.editor.Buffer;
import com.google.collide.client.editor.Editor;
import com.google.collide.client.editor.ViewportModel;
import com.google.collide.client.editor.gutter.Gutter;
import com.google.collide.client.editor.selection.SelectionModel;
import com.google.collide.client.util.JsIntegerMap;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.util.JsonCollections;
import com.google.collide.shared.util.ListenerRegistrar;
import com.google.gwt.resources.client.ClientBundle;

import elemental.css.CSSStyleDeclaration;
import elemental.dom.Element;

/**
 * A renderer for the line numbers in the left gutter.
 */
public class LineNumberRenderer {

  private static final int NONE = -1;

  private final Buffer buffer;
  private final Gutter leftGutter;

  /**
   * Current editor instance.
   *
   * Used to track if current fie can be edited (i.e. is not readonly).
   *
   * TODO: add new abstraction to avoid editor passing.
   */
  private final Editor editor;

  private int previousBottomLineNumber = -1;
  private int previousTopLineNumber = -1;
  private JsIntegerMap<Element> lineNumberToElementCache;
  private final ViewportModel viewport;
  private final Css css;
  private int activeLineNumber = NONE;
  private int renderedActiveLineNumber = NONE;
  private final JsonArray<ListenerRegistrar.Remover> listenerRemovers =
      JsonCollections.createArray();

  private final SelectionModel.CursorListener cursorListener = new SelectionModel.CursorListener() {
    @Override
    public void onCursorChange(LineInfo lineInfo, int column, boolean isExplicitChange) {
      activeLineNumber = lineInfo.number();
      updateActiveLine();
    }
  };

  private Editor.ReadOnlyListener readonlyListener = new Editor.ReadOnlyListener() {
    @Override
    public void onReadOnlyChanged(boolean isReadOnly) {
      updateActiveLine();
    }
  };

  private void updateActiveLine() {
    int lineNumber = this.activeLineNumber;
    if (editor.isReadOnly()) {
      lineNumber = NONE;
    }
    if (lineNumber == renderedActiveLineNumber) {
      return;
    }

    if (renderedActiveLineNumber != NONE) {
      Element renderedActiveLine = lineNumberToElementCache.get(renderedActiveLineNumber);
      if (renderedActiveLine != null) {
        renderedActiveLine.removeClassName(css.activeLineNumber());
        renderedActiveLineNumber = NONE;
      }
    }
    Element newActiveLine = lineNumberToElementCache.get(lineNumber);
    // Add class if it's in the viewport.
    if (newActiveLine != null) {
      newActiveLine.addClassName(css.activeLineNumber());
      renderedActiveLineNumber = lineNumber;
    }
  }

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

  /**
   * Line number CSS.
   */
  public interface Css extends Editor.EditorSharedCss {
    String lineNumber();

    String activeLineNumber();
  }

  /**
   * Line number resources.
   */
  public interface Resources extends ClientBundle {
    @Source({"collide/client/common/constants.css",
        "LineNumberRenderer.css"})
    Css lineNumberRendererCss();
  }

  LineNumberRenderer(Buffer buffer, Resources res, Gutter leftGutter, ViewportModel viewport,
      SelectionModel selection, Editor editor) {
    this.buffer = buffer;
    this.leftGutter = leftGutter;
    this.editor = editor;
    this.lineNumberToElementCache = JsIntegerMap.create();
    this.viewport = viewport;
    this.css = res.lineNumberRendererCss();
    listenerRemovers.add(selection.getCursorListenerRegistrar().add(cursorListener));
    listenerRemovers.add(editor.getReadOnlyListenerRegistrar().add(readonlyListener));
  }

  void renderImpl(int updateBeginLineNumber) {
    int topLineNumber = viewport.getTopLineNumber();
    int bottomLineNumber = viewport.getBottomLineNumber();

    if (previousBottomLineNumber == -1 || topLineNumber > previousBottomLineNumber
        || bottomLineNumber < previousTopLineNumber) {

      if (previousBottomLineNumber > -1) {
        garbageCollectLines(previousTopLineNumber, previousBottomLineNumber);
      }

      fillOrUpdateLines(topLineNumber, bottomLineNumber);

    } else {
      /*
       * The viewport was shifted and part of the old viewport will be in the
       * new viewport.
       */
      // first garbage collect any lines that have gone off the screen
      if (previousTopLineNumber < topLineNumber) {
        // off the top
        garbageCollectLines(previousTopLineNumber, topLineNumber - 1);
      }

      if (previousBottomLineNumber > bottomLineNumber) {
        // off the bottom
        garbageCollectLines(bottomLineNumber + 1, previousBottomLineNumber);
      }

      /*
       * Re-create any line numbers that are now visible or have had their
       * positions shifted.
       */
      if (previousTopLineNumber > topLineNumber) {
        // new lines at the top
        fillOrUpdateLines(topLineNumber, previousTopLineNumber - 1);
      }

      if (updateBeginLineNumber >= 0 && updateBeginLineNumber <= bottomLineNumber) {
        // lines updated in the middle; redraw everything below
        fillOrUpdateLines(updateBeginLineNumber, bottomLineNumber);
      } else {
        // only check new lines scrolled in from the bottom
        if (previousBottomLineNumber < bottomLineNumber) {
          fillOrUpdateLines(previousBottomLineNumber, bottomLineNumber);
        }
      }
    }

    previousTopLineNumber = viewport.getTopLineNumber();
    previousBottomLineNumber = viewport.getBottomLineNumber();
  }

  void render() {
    renderImpl(-1);
  }

  /**
   * Re-render all line numbers including and after lineNumber to account for
   * spacer movement.
   */
  void renderLineAndFollowing(int lineNumber) {
    renderImpl(lineNumber);
  }

  private void fillOrUpdateLines(int beginLineNumber, int endLineNumber) {
    for (int i = beginLineNumber; i <= endLineNumber; i++) {
      Element lineElement = lineNumberToElementCache.get(i);
      if (lineElement != null) {
        updateElementPosition(lineElement, i);
      } else {
        Element element = createElement(i);
        lineNumberToElementCache.put(i, element);
        leftGutter.addUnmanagedElement(element);
      }
    }
  }

  private void updateElementPosition(Element lineNumberElement, int lineNumber) {
    lineNumberElement.getStyle().setTop(
        buffer.calculateLineTop(lineNumber), CSSStyleDeclaration.Unit.PX);
  }

  private Element createElement(int lineNumber) {
    Element element = Elements.createDivElement(css.lineNumber());
    // Line 0 will be rendered as Line 1
    element.setTextContent(String.valueOf(lineNumber + 1));
    element.getStyle().setTop(buffer.calculateLineTop(lineNumber), CSSStyleDeclaration.Unit.PX);
    if (lineNumber == activeLineNumber) {
      element.addClassName(css.activeLineNumber());
      renderedActiveLineNumber = activeLineNumber;
    }
    return element;
  }

  private void garbageCollectLines(int beginLineNumber, int endLineNumber) {
    for (int i = beginLineNumber; i <= endLineNumber; i++) {
      Element lineElement = lineNumberToElementCache.get(i);
      if (lineElement != null) {
        leftGutter.removeUnmanagedElement(lineElement);
        lineNumberToElementCache.erase(i);
      } else {
        throw new IndexOutOfBoundsException(
            "Tried to garbage collect line number " + i + " when it does not exist.");
      }
    }
    if (beginLineNumber <= renderedActiveLineNumber && renderedActiveLineNumber <= endLineNumber) {
      renderedActiveLineNumber = NONE;
    }
  }
}
