// 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.autocomplete.integration;

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

import collide.client.util.CssUtils;
import collide.client.util.Elements;

import com.google.collide.client.code.autocomplete.AutocompleteBox;
import com.google.collide.client.code.autocomplete.AutocompleteProposal;
import com.google.collide.client.code.autocomplete.AutocompleteProposals;
import com.google.collide.client.code.autocomplete.SignalEventEssence;
import com.google.collide.client.editor.Editor;
import com.google.collide.client.editor.FocusManager;
import com.google.collide.client.ui.list.SimpleList;
import com.google.collide.client.ui.list.SimpleList.View;
import com.google.collide.client.ui.menu.AutoHideController;
import com.google.collide.client.util.dom.DomUtils;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.document.anchor.ReadOnlyAnchor;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.resources.client.CssResource;

import elemental.css.CSSStyleDeclaration;
import elemental.dom.Element;
import elemental.html.ClientRect;
import elemental.html.TableCellElement;
import elemental.html.TableElement;

/**
 * A controller for managing the UI for showing autocomplete proposals.
 *
 */
public class AutocompleteUiController implements AutocompleteBox {

  public interface Resources extends SimpleList.Resources {
    @Source("AutocompleteComponent.css")
    Css autocompleteComponentCss();
  }

  public interface Css extends CssResource {
    String cappedProposalLabel();

    String proposalLabel();

    String proposalGroup();

    String container();

    String items();

    String hint();

    int maxHeight();
  }

  private static final int MAX_COMPLETIONS_TO_SHOW = 100;
  private static final AutocompleteProposal CAPPED_INDICATOR = new AutocompleteProposal("");

  private final SimpleList.ListItemRenderer<AutocompleteProposal> listItemRenderer =
      new SimpleList.ListItemRenderer<AutocompleteProposal>() {
        @Override
        public void render(Element itemElement, AutocompleteProposal itemData) {
          TableCellElement label = Elements.createTDElement(css.proposalLabel());
          TableCellElement group = Elements.createTDElement(css.proposalGroup());

          if (itemData != CAPPED_INDICATOR) {
            label.setTextContent(itemData.getLabel());
            group.setTextContent(itemData.getPath().getPathString());
          } else {
            label.setTextContent("Type for more results");
            label.addClassName(css.cappedProposalLabel());
          }

          itemElement.appendChild(label);
          itemElement.appendChild(group);
        }

        @Override
        public Element createElement() {
          return Elements.createTRElement();
        }
      };

  private final SimpleList.ListEventDelegate<AutocompleteProposal> listDelegate =
      new SimpleList.ListEventDelegate<AutocompleteProposal>() {
        @Override
        public void onListItemClicked(Element itemElement, AutocompleteProposal itemData) {
          Preconditions.checkNotNull(delegate);
          if (itemData == CAPPED_INDICATOR) {
            return;
          }

          delegate.onSelect(autocompleteProposals.select(itemData));
        }
      };

  private final AutoHideController autoHideController;
  private final Css css;
  private final SimpleList<AutocompleteProposal> list;
  private Events delegate;
  private final Editor editor;
  private final Element box;
  private final Element container;
  private final Element hint;

  /** Will be non-null when the popup is showing */
  private ReadOnlyAnchor anchor;

  /**
   * True to force the layout above the anchor, false to layout below. This
   * should be set when showing from the hidden state. It's used to keep
   * the position consistent while the box is visible.
   */
  private boolean positionAbove;

  /**
   * The currently displayed proposals. This may contain more proposals than actually shown since we
   * cap the maximum number of visible proposals. This will be null if the UI is not showing.
   */
  private AutocompleteProposals autocompleteProposals;

  public AutocompleteUiController(Editor editor, Resources res) {
    this.editor = editor;
    this.css = res.autocompleteComponentCss();

    box = Elements.createDivElement();
    // Prevent our mouse events from going to the editor
    DomUtils.stopMousePropagation(box);

    TableElement tableElement = Elements.createTableElement();
    tableElement.setClassName(css.items());

    container = Elements.createDivElement(css.container());
    DomUtils.preventExcessiveScrollingPropagation(container);
    container.appendChild(tableElement);
    box.appendChild(container);

    hint = Elements.createDivElement(css.hint());
    CssUtils.setDisplayVisibility2(hint, false);
    box.appendChild(hint);

    list =
        SimpleList.create((View) box, container, tableElement, res.defaultSimpleListCss(),
            listItemRenderer, listDelegate);

    autoHideController = AutoHideController.create(box);
    autoHideController.setCaptureOutsideClickOnClose(false);
    autoHideController.setDelay(-1);
  }

  @Override
  public boolean isShowing() {
    return autoHideController.isShowing();
  }

  @Override
  public boolean consumeKeySignal(SignalEventEssence signal) {
    Preconditions.checkState(isShowing());
    Preconditions.checkNotNull(delegate);

    if ((signal.keyCode == KeyCodes.KEY_TAB) || (signal.keyCode == KeyCodes.KEY_ENTER)) {
      delegate.onSelect(autocompleteProposals.select(list.getSelectionModel().getSelectedItem()));
      return true;
    }

    if (signal.keyCode == KeyCodes.KEY_ESCAPE) {
      delegate.onCancel();
      return true;
    }

    if (signal.type != SignalEvent.KeySignalType.NAVIGATION) {
      return false;
    }

    if ((signal.keyCode == KeyCodes.KEY_DOWN)) {
      list.getSelectionModel().selectNext();
      return true;
    }

    if (signal.keyCode == KeyCodes.KEY_UP) {
      list.getSelectionModel().selectPrevious();
      return true;
    }

    if ((signal.keyCode == KeyCodes.KEY_LEFT) || (signal.keyCode == KeyCodes.KEY_RIGHT)) {
      delegate.onCancel();
      return true;
    }

    if (signal.keyCode == KeyCodes.KEY_PAGEUP) {
      list.getSelectionModel().selectPreviousPage();
      return true;
    }

    if (signal.keyCode == KeyCodes.KEY_PAGEDOWN) {
      list.getSelectionModel().selectNextPage();
      return true;
    }

    return false;
  }

  @Override
  public void setDelegate(Events delegate) {
    this.delegate = delegate;
  }

  @Override
  public void dismiss() {
    boolean hadFocus = list.hasFocus();
    autoHideController.hide();

    if (anchor != null) {
      editor.getBuffer().removeAnchoredElement(anchor, autoHideController.getView().getElement());
      anchor = null;
    }

    autocompleteProposals = null;

    FocusManager focusManager = editor.getFocusManager();
    if (hadFocus && !focusManager.hasFocus()) {
      focusManager.focus();
    }
  }

  @Override
  public void positionAndShow(AutocompleteProposals items) {
    this.autocompleteProposals = items;
    this.anchor = editor.getSelection().getCursorAnchor();

    boolean showingFromHidden = !autoHideController.isShowing();
    if (showingFromHidden) {
      list.getSelectionModel().clearSelection();
    }

    final JsonArray<AutocompleteProposal> itemsToDisplay;
    if (items.size() <= MAX_COMPLETIONS_TO_SHOW) {
      itemsToDisplay = items.getItems();
    } else {
      itemsToDisplay = items.getItems().slice(0, MAX_COMPLETIONS_TO_SHOW);
      itemsToDisplay.add(CAPPED_INDICATOR);
    }
    list.render(itemsToDisplay);
    if (list.getSelectionModel().getSelectedItem() == null) {
      list.getSelectionModel().setSelectedItem(0);
    }

    String hintText = items.getHint();
    if (hintText == null) {
      hint.setTextContent("");
      CssUtils.setDisplayVisibility2(hint, false);
    } else {
      hint.setTextContent(hintText);
      CssUtils.setDisplayVisibility2(hint, true);
    }

    autoHideController.show();

    editor.getBuffer().addAnchoredElement(anchor, box);

    ensureRootElementWillBeOnScreen(showingFromHidden);
  }

  private void ensureRootElementWillBeOnScreen(boolean showingFromHidden) {
    // Remove any max-heights so we can get its desired height
    container.getStyle().removeProperty("max-height");
    ClientRect bounds = box.getBoundingClientRect();
    int height = (int) bounds.getHeight();
    int delta = height - (int) container.getBoundingClientRect().getHeight();

    ClientRect bufferBounds = editor.getBuffer().getBoundingClientRect();
    int lineHeight = editor.getBuffer().getEditorLineHeight();
    int lineTop = (int) bounds.getTop() - CssUtils.parsePixels(box.getStyle().getMarginTop());

    int spaceAbove =  lineTop - (int) bufferBounds.getTop();
    int spaceBelow = (int) bufferBounds.getBottom() - lineTop - lineHeight;

    if (showingFromHidden) {
      // If it was already showing, we don't adjust the positioning.
      positionAbove = spaceAbove >= css.maxHeight() && spaceBelow < css.maxHeight();
    }

    // Get available height.
    int maxHeight = positionAbove ? spaceAbove : spaceBelow;

    // Restrict to specified height.
    maxHeight = Math.min(maxHeight, css.maxHeight());

    // Fit to content size.
    maxHeight = Math.min(maxHeight, height);

    container.getStyle().setProperty(
        "max-height", (maxHeight - delta) + CSSStyleDeclaration.Unit.PX);

    int marginTop = positionAbove ? -maxHeight : lineHeight;
    box.getStyle().setMarginTop(marginTop, CSSStyleDeclaration.Unit.PX);
  }

  @VisibleForTesting
  SimpleList<AutocompleteProposal> getList() {
    return list;
  }
}
