// 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.code.popup;

import javax.annotation.Nullable;

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.renderer.SingleChunkLineRenderer;
import com.google.collide.client.ui.menu.AutoHideComponent;
import com.google.collide.client.ui.menu.PositionController.HorizontalAlign;
import com.google.collide.client.ui.menu.PositionController.Position;
import com.google.collide.client.ui.menu.PositionController.PositionerBuilder;
import com.google.collide.client.ui.menu.PositionController.VerticalAlign;
import com.google.collide.client.ui.popup.Popup;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.Document;
import com.google.collide.shared.document.LineInfo;
import com.google.collide.shared.document.anchor.Anchor;
import com.google.collide.shared.document.anchor.AnchorType;

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

/**
 * Controller for the editor-wide popup.
 */
public class EditorPopupController {

  private static final AnchorType START_ANCHOR_TYPE = AnchorType.create(
      EditorPopupController.class, "startAnchor");

  private static final AnchorType END_ANCHOR_TYPE = AnchorType.create(
      EditorPopupController.class, "endAnchor");

  /**
   * Interface for specifying an arbitrary renderer for the popup.
   */
  public interface PopupRenderer {

    /**
     * @return rendered content of the popup
     */
    Element renderDom();
  }

  /**
   * Interface for controlling the popup after it has been shown
   * or scheduled to be shown.
   */
  public interface Remover {

    /**
     * @return true if this popup is currently visible or timer is running
     *         to make it visible
     */
    public boolean isVisibleOrPending();

    /**
     * Hides this popup, if it is currently shown or cancels pending show.
     */
    public void remove();
  }

  public static EditorPopupController create(Popup.Resources resources, Editor editor) {
    return new EditorPopupController(Popup.create(resources), editor);
  }

  private final Editor editor;
  private final Popup popup;
  /** Used to change the position of the popup each time it is shown */
  private final PositionerBuilder positionerBuilder;
  private Remover currentPopupRemover;

  /**
   * A DIV that floats on top of the editor area. We attach the popup to this
   * element.
   */
  private final Element popupDummyElement;

  /**
   * The {@link #popupDummyElement} is anchored between {@code #startAnchor} and
   * {@link #endAnchor} in the {@link #document} document. These variables are
   * tracked to properly detach the anchors from the original document.
   */
  private Anchor startAnchor;
  private Anchor endAnchor;
  private Document document;

  private final Anchor.ShiftListener anchorShiftListener = new Anchor.ShiftListener() {
    @Override
    public void onAnchorShifted(Anchor anchor) {
      updatePopupDummyElementWidth();
    }
  };

  private final Buffer.ScrollListener scrollListener = new Buffer.ScrollListener() {
    @Override
    public void onScroll(Buffer buffer, int scrollTop) {
      hide();
    }
  };

  private EditorPopupController(Popup popup, Editor editor) {
    this.popup = popup;
    this.editor = editor;
    this.popupDummyElement = createPopupDummyElement(editor.getBuffer().getEditorLineHeight());
    this.positionerBuilder = new PositionerBuilder().setPosition(Position.NO_OVERLAP)
        .setHorizontalAlign(HorizontalAlign.MIDDLE);

    popup.setAutoHideHandler(new AutoHideComponent.AutoHideHandler() {
      @Override
      public void onHide() {
        hide();
      }

      public void onShow() {
        // do nothing
      }
    });
  }

  public void cleanup() {
    hide();
  }
  
  private static Element createPopupDummyElement(int lineHeight) {
    Element element = Elements.createDivElement();

    CSSStyleDeclaration style = element.getStyle();
    style.setDisplay(CSSStyleDeclaration.Display.INLINE_BLOCK);
    style.setPosition(CSSStyleDeclaration.Position.ABSOLUTE);
    style.setWidth(0, CSSStyleDeclaration.Unit.PX);
    style.setHeight(lineHeight, CSSStyleDeclaration.Unit.PX);
    style.setZIndex(1);

    // We do this so that custom CSS class (provided by textCssClassName in the #showPopup)
    // with cursor:pointer should work correctly.
    style.setProperty("pointer-events", "none");

    return element;
  }

  private void attachPopupDummyElement(LineInfo lineInfo, int startColumn, int endColumn) {
    // Detach from the old document first, just in case.
    detachPopupDummyElement();

    document = editor.getDocument();
    startAnchor = document.getAnchorManager().createAnchor(START_ANCHOR_TYPE,
        lineInfo.line(), lineInfo.number(), startColumn);
    startAnchor.setRemovalStrategy(Anchor.RemovalStrategy.SHIFT);
    startAnchor.getShiftListenerRegistrar().add(anchorShiftListener);

    endAnchor = document.getAnchorManager().createAnchor(END_ANCHOR_TYPE,
        lineInfo.line(), lineInfo.number(), endColumn);
    endAnchor.setRemovalStrategy(Anchor.RemovalStrategy.SHIFT);
    endAnchor.getShiftListenerRegistrar().add(anchorShiftListener);

    editor.getBuffer().addAnchoredElement(startAnchor, popupDummyElement);

    updatePopupDummyElementWidth();
  }

  private void updatePopupDummyElementWidth() {
    if (startAnchor != null && endAnchor != null) {
      Buffer buffer = editor.getBuffer();
      int left = buffer.calculateColumnLeft(startAnchor.getLine(), startAnchor.getColumn());
      popupDummyElement.getStyle().setWidth(
          buffer.calculateColumnLeft(endAnchor.getLine(), endAnchor.getColumn() + 1) - left,
          CSSStyleDeclaration.Unit.PX);
    }
  }

  private void detachPopupDummyElement() {
    if (startAnchor != null) {
      startAnchor.getShiftListenerRegistrar().remove(anchorShiftListener);
      editor.getBuffer().removeAnchoredElement(startAnchor, popupDummyElement);
      document.getAnchorManager().removeAnchor(startAnchor);
      startAnchor = null;
    }
    if (endAnchor != null) {
      endAnchor.getShiftListenerRegistrar().remove(anchorShiftListener);
      document.getAnchorManager().removeAnchor(endAnchor);
      endAnchor = null;
    }
    document = null;
  }

  /**
   * Shows the popup anchored to a given position in the line.
   *
   * @param lineInfo the line to anchor the popup to
   * @param startColumn start column in the line, inclusive
   * @param endColumn end column in the line, inclusive
   * @param textCssClassName class name to highlight the anchor, or {@code null}
   *        if this is not needed
   * @param renderer the popup renderer
   * @param popupPartnerElements array of partner element of the popup
   *        (i.e. those DOM elements where mouse hover will not trigger closing
   *        the popup), or {@code null} if no additional partners should be
   *        considered. Also see {@link AutoHideComponent#addPartner}
   * @param verticalAlign vertical align of the popup related to the line
   * @param shouldCaptureOutsideClickOnClose whether the popup should capture
   *        and prevent clicks outside of it when it closes itself
   * @return an instance of {@link Remover} to control the popup
   */
  public Remover showPopup(LineInfo lineInfo, int startColumn, int endColumn,
      @Nullable String textCssClassName, PopupRenderer renderer,
      final @Nullable JsonArray<Element> popupPartnerElements,
      final VerticalAlign verticalAlign, boolean shouldCaptureOutsideClickOnClose,
      int delayMs) {
    hide();

    attachPopupDummyElement(lineInfo, startColumn, endColumn);

    final SingleChunkLineRenderer lineRenderer = textCssClassName == null ? null :
        SingleChunkLineRenderer.create(startAnchor, endAnchor, textCssClassName);

    popup.setContentElement(renderer.renderDom());
    popup.setCaptureOutsideClickOnClose(shouldCaptureOutsideClickOnClose);
    setPopupPartnersEnabled(popupPartnerElements, true);

    final Timer showTimer = new Timer() {
      @Override
      public void run() {
        positionerBuilder.setVerticalAlign(verticalAlign);
        popup.show(positionerBuilder.buildAnchorPositioner(popupDummyElement));
        if (lineRenderer != null) {
          editor.addLineRenderer(lineRenderer);
          requestRenderLines(lineRenderer);
        }
      }
    };
    if (delayMs <= 0) {
      showTimer.run();
    } else {
      showTimer.schedule(delayMs);
    }

    final com.google.collide.shared.util.ListenerRegistrar.Remover scrollListenerRemover =
        editor.getBuffer().getScrollListenerRegistrar().add(scrollListener);

    return (currentPopupRemover = new Remover() {
      @Override
      public boolean isVisibleOrPending() {
        return this == currentPopupRemover;
      }

      @Override
      public void remove() {
        showTimer.cancel();
        if (isVisibleOrPending()) {
          currentPopupRemover = null;
          detachPopupDummyElement();

          setPopupPartnersEnabled(popupPartnerElements, false);
          popup.destroy();

          if (lineRenderer != null) {
            editor.removeLineRenderer(lineRenderer);
            requestRenderLines(lineRenderer);
          }

          scrollListenerRemover.remove();
        }
      }
    });
  }

  public void cancelPendingHide() {
    popup.cancelPendingHide();
  }

  private void requestRenderLines(SingleChunkLineRenderer lineRenderer) {
    for (int i = lineRenderer.startLine(); i <= lineRenderer.endLine(); ++i) {
      editor.getRenderer().requestRenderLine(
          editor.getDocument().getLineFinder().findLine(i).line());
    }
  }

  private void setPopupPartnersEnabled(@Nullable JsonArray<Element> partnerElements,
      boolean enable) {
    if (partnerElements != null) {
      for (int i = 0, n = partnerElements.size(); i < n; ++i) {
        Element element = partnerElements.get(i);
        if (enable) {
          popup.addPartner(element);
          popup.addPartnerClickTargets(element);
        } else {
          popup.removePartner(element);
          popup.removePartnerClickTargets(element);
        }
      }
    }
  }

  /**
   * Hides the popup if visible.
   */
  public void hide() {
    if (currentPopupRemover != null) {
      currentPopupRemover.remove();
      currentPopupRemover = null;
    }
  }
}
