package com.google.gwt.user.client.ui;

import java.util.Collection;
import java.util.List;

import com.google.gwt.editor.client.IsEditor;
import com.google.gwt.editor.client.adapters.TakesValueEditor;
import com.google.gwt.event.dom.client.HandlesAllKeyEvents;
import com.google.gwt.event.dom.client.HasAllKeyHandlers;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.dom.client.KeyPressHandler;
import com.google.gwt.event.dom.client.KeyUpEvent;
import com.google.gwt.event.dom.client.KeyUpHandler;
import com.google.gwt.event.logical.shared.HasSelectionHandlers;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.text.shared.Renderer;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.PopupPanel.AnimationType;
import com.google.gwt.user.client.ui.ValueSuggestOracle.ValueCallback;
import com.google.gwt.user.client.ui.ValueSuggestOracle.ValueRequest;
import com.google.gwt.user.client.ui.ValueSuggestOracle.ValueResponse;
import com.google.gwt.user.client.ui.ValueSuggestOracle.ValueSuggestion;

public class ValueSuggestBox<T> extends Composite implements HasValue<T>,
    IsEditor<TakesValueEditor<T>>, HasAllKeyHandlers, HasSelectionHandlers<ValueSuggestion<T>> {

  public static interface ValueSuggestionCallback<T> {
    void onSuggestionSelected(ValueSuggestion<T> suggestion);
  }
  public abstract static class ValueSuggestionDisplay<T> {

    protected abstract ValueSuggestion<T> getCurrentSelection();

    protected abstract void hideSuggestions();

    protected abstract void moveSelectionDown();

    protected abstract void moveSelectionUp();

    protected void onEnsureDebugId(final String suggestBoxBaseID) {
    }

    protected void setMoreSuggestions(final boolean hasMoreSuggestions, final int numMoreSuggestions) {

    }

    protected abstract void showSuggestions(ValueSuggestBox<T> suggestBox,
        Collection<? extends ValueSuggestion<T>> suggestions, boolean isDisplayStringHTML,
        boolean isAutoSelectEnabled, ValueSuggestionCallback<T> callback);
  }
  public static class DefaultSuggestionDisplay<T> extends ValueSuggestionDisplay<T> implements
      HasAnimation {

    private final ValueSuggestionMenu suggestionMenu;
    private final PopupPanel suggestionPopup;

    private ValueSuggestBox<T> lastSuggestBox = null;

    private boolean hideWhenEmpty = true;

    private UIObject positionRelativeTo;

    public DefaultSuggestionDisplay() {
      suggestionMenu = new ValueSuggestionMenu(true);
      suggestionPopup = createPopup();
      suggestionPopup.setWidget(decorateSuggestionList(suggestionMenu));
    }

    @Override
    public void hideSuggestions() {
      suggestionPopup.hide();
    }

    @Override
    public boolean isAnimationEnabled() {
      return suggestionPopup.isAnimationEnabled();
    }

    public boolean isSuggestionListHiddenWhenEmpty() {
      return hideWhenEmpty;
    }

    public boolean isSuggestionListShowing() {
      return suggestionPopup.isShowing();
    }

    @Override
    public void setAnimationEnabled(final boolean enable) {
      suggestionPopup.setAnimationEnabled(enable);
    }

    public void setPopupStyleName(final String style) {
      suggestionPopup.setStyleName(style);
    }

    public void setPositionRelativeTo(final UIObject uiObject) {
      positionRelativeTo = uiObject;
    }

    public void setSuggestionListHiddenWhenEmpty(final boolean hideWhenEmpty) {
      this.hideWhenEmpty = hideWhenEmpty;
    }

    protected PopupPanel createPopup() {
      final PopupPanel p = new DecoratedPopupPanel(true, false, "suggestPopup");
      p.setStyleName("gwt-SuggestBoxPopup");
      p.setPreviewingAllNativeEvents(true);
      p.setAnimationType(AnimationType.ROLL_DOWN);
      return p;
    }

    protected Widget decorateSuggestionList(final Widget suggestionList) {
      return suggestionList;
    }

    @Override
    protected ValueSuggestion<T> getCurrentSelection() {
      if (!isSuggestionListShowing()) {
        return null;
      }
      final MenuItem item = suggestionMenu.getSelectedItem();
      return item == null ? null : ((ValueSuggestionMenuItem<T>) item).getSuggestion();
    }

    protected PopupPanel getPopupPanel() {
      return suggestionPopup;
    }

    @Override
    protected void moveSelectionDown() {
      // Make sure that the menu is actually showing. These keystrokes
      // are only relevant when choosing a suggestion.
      if (isSuggestionListShowing()) {
        // If nothing is selected, getSelectedItemIndex will return -1 and we
        // will select index 0 (the first item) by default.
        suggestionMenu.selectItem(suggestionMenu.getSelectedItemIndex() + 1);
      }
    }

    @Override
    protected void moveSelectionUp() {
      // Make sure that the menu is actually showing. These keystrokes
      // are only relevant when choosing a suggestion.
      if (isSuggestionListShowing()) {
        // if nothing is selected, then we should select the last suggestion by
        // default. This is because, in some cases, the suggestions menu will
        // appear above the text box rather than below it (for example, if the
        // text box is at the bottom of the window and the suggestions will not
        // fit below the text box). In this case, users would expect to be able
        // to use the up arrow to navigate to the suggestions.
        if (suggestionMenu.getSelectedItemIndex() == -1) {
          suggestionMenu.selectItem(suggestionMenu.getNumItems() - 1);
        } else {
          suggestionMenu.selectItem(suggestionMenu.getSelectedItemIndex() - 1);
        }
      }
    }

    /**
     * <b>Affected Elements:</b>
     * <ul>
     * <li>-popup = The popup that appears with suggestions.</li>
     * <li>-item# = The suggested item at the specified index.</li>
     * </ul>
     * 
     * @see UIObject#onEnsureDebugId(String)
     */
    @Override
    protected void onEnsureDebugId(final String baseID) {
      suggestionPopup.ensureDebugId(baseID + "-popup");
      suggestionMenu.setMenuItemDebugIds(baseID);
    }

    @Override
    protected void showSuggestions(final ValueSuggestBox<T> suggestBox,
        final Collection<? extends ValueSuggestion<T>> suggestions,
        final boolean isDisplayStringHTML, final boolean isAutoSelectEnabled,
        final ValueSuggestionCallback<T> callback) {
      // Hide the popup if there are no suggestions to display.
      final boolean anySuggestions = (suggestions != null && suggestions.size() > 0);
      if (!anySuggestions && hideWhenEmpty) {
        hideSuggestions();
        return;
      }

      // Hide the popup before we manipulate the menu within it. If we do not
      // do this, some browsers will redraw the popup as items are removed
      // and added to the menu.
      if (suggestionPopup.isAttached()) {
        suggestionPopup.hide();
      }

      suggestionMenu.clearItems();

      for (final ValueSuggestion<T> curSuggestion : suggestions) {
        final ValueSuggestionMenuItem<T> menuItem =
            new ValueSuggestionMenuItem<T>(curSuggestion, isDisplayStringHTML);
        menuItem.setCommand(new Command() {
          @Override
          public void execute() {
            callback.onSuggestionSelected(curSuggestion);
          }
        });

        suggestionMenu.addItem(menuItem);
      }

      if (isAutoSelectEnabled && anySuggestions) {
        // Select the first item in the suggestion menu.
        suggestionMenu.selectItem(0);
      }

      // Link the popup autoHide to the TextBox.
      if (lastSuggestBox != suggestBox) {
        // If the suggest box has changed, free the old one first.
        if (lastSuggestBox != null) {
          suggestionPopup.removeAutoHidePartner(lastSuggestBox.getElement());
        }
        lastSuggestBox = suggestBox;
        suggestionPopup.addAutoHidePartner(suggestBox.getElement());
      }

      // Show the popup under the TextBox.
      suggestionPopup.showRelativeTo(positionRelativeTo != null ? positionRelativeTo : suggestBox);
    }

  }
  private static class ValueSuggestionMenu extends MenuBar {

    public ValueSuggestionMenu(final boolean vertical) {
      super(vertical);
      // Make sure that CSS styles specified for the default Menu classes
      // do not affect this menu
      setStyleName("");
      setFocusOnHoverEnabled(false);
    }

    public void doSelectedItemAction() {
      // In order to perform the action of the item that is currently
      // selected, the menu must be showing.
      final MenuItem selectedItem = getSelectedItem();
      if (selectedItem != null) {
        doItemAction(selectedItem, true, false);
      }
    }

    public int getNumItems() {
      return getItems().size();
    }

    /**
     * Returns the index of the menu item that is currently selected.
     * 
     * @return returns the selected item
     */
    public int getSelectedItemIndex() {
      // The index of the currently selected item can only be
      // obtained if the menu is showing.
      final MenuItem selectedItem = getSelectedItem();
      if (selectedItem != null) {
        return getItems().indexOf(selectedItem);
      }
      return -1;
    }

    public void selectItem(final int index) {
      final List<MenuItem> items = getItems();
      if (index > -1 && index < items.size()) {
        itemOver(items.get(index), false);
      }
    }
  }
  private static class ValueSuggestionMenuItem<T> extends MenuItem {

    private static final String STYLENAME_DEFAULT = "item";

    private ValueSuggestion<T> suggestion;

    public ValueSuggestionMenuItem(final ValueSuggestion<T> suggestion, final boolean asHTML) {
      super(suggestion.getDisplayString(), asHTML);

      DOM.setStyleAttribute(getElement(), "whiteSpace", "nowrap");
      setStyleName(STYLENAME_DEFAULT);
      setSuggestion(suggestion);
    }

    public ValueSuggestion<T> getSuggestion() {
      return suggestion;
    }

    public void setSuggestion(final ValueSuggestion<T> suggestion) {
      this.suggestion = suggestion;
    }
  }

  private int limit = 20;
  private boolean selectsFirstItem = true;
  private ValueSuggestOracle<T> oracle;
  private String currentRenderedValue;
  private T currentValue;
  private final TakesValueEditor<T> editor;
  private final ValueSuggestionDisplay<T> display;
  private final Renderer<T> renderer;
  // private final TextBox box;
  private final ValueCallback<T> callback = new ValueCallback<T>() {
    @Override
    public void onSuggestionsReady(final ValueRequest request, final ValueResponse<T> response) {
      display.setMoreSuggestions(response.hasMoreSuggestions(), response.getMoreSuggestionsCount());
      display.showSuggestions(ValueSuggestBox.this, response.getSuggestions(), oracle
          .isDisplayStringHTML(), isAutoSelectEnabled(), suggestionCallback);
    }
  };
  private final ValueSuggestionCallback<T> suggestionCallback = new ValueSuggestionCallback<T>() {
    @Override
    public void onSuggestionSelected(final ValueSuggestion<T> suggestion) {
      setNewSelection(suggestion);
    }
  };

  public ValueSuggestBox(final ValueSuggestOracle<T> oracle, final Renderer<T> renderer) {
    this(oracle, new DefaultSuggestionDisplay<T>(), renderer);
  }

  public ValueSuggestBox(final ValueSuggestOracle<T> oracle,
      final ValueSuggestionDisplay<T> suggestDisplay, final Renderer<T> renderer) {
    this.display = suggestDisplay;
    this.renderer = renderer;
    initWidget(new TextBox());
    editor = TakesValueEditor.of(this);
    addEventsToTextBox();

    setOracle(oracle);
    setStyleName(STYLENAME_DEFAULT);
  }

  private void setOracle(final ValueSuggestOracle<T> oracle) {
    this.oracle = oracle;
  }

  @Override
  public HandlerRegistration addKeyDownHandler(final KeyDownHandler handler) {
    return addDomHandler(handler, KeyDownEvent.getType());
  }

  @Override
  public HandlerRegistration addKeyPressHandler(final KeyPressHandler handler) {
    return addDomHandler(handler, KeyPressEvent.getType());
  }

  @Override
  public HandlerRegistration addKeyUpHandler(final KeyUpHandler handler) {
    return addDomHandler(handler, KeyUpEvent.getType());
  }

  @Override
  public HandlerRegistration addSelectionHandler(final SelectionHandler<ValueSuggestion<T>> handler) {
    return addHandler(handler, SelectionEvent.getType());
  }

  @Override
  public HandlerRegistration addValueChangeHandler(final ValueChangeHandler<T> handler) {
    return addHandler(handler, ValueChangeEvent.getType());
  }

  @Override
  public TakesValueEditor<T> asEditor() {
    return editor;
  }

  public int getLimit() {
    return limit;
  }

  public ValueSuggestionDisplay<T> getSuggestionDisplay() {
    return display;
  }

  public ValueSuggestOracle<T> getSuggestOracle() {
    return oracle;
  }

  public int getTabIndex() {
    return getTextBox().getTabIndex();
  }

  public String getText() {
    return getTextBox().getText();
  }

  private TextBox getTextBox() {
    return (TextBox) getWidget();
  }

  @Override
  public T getValue() {
    return currentValue;
  }

  public boolean isAutoSelectEnabled() {
    return selectsFirstItem;
  }

  public void setAccessKey(final char key) {
    getTextBox().setAccessKey(key);
  }

  /**
   * Turns on or off the behavior that automatically selects the first suggested item. This behavior
   * is on by default.
   * 
   * @param selectsFirstItem Whether or not to automatically select the first suggestion
   */
  public void setAutoSelectEnabled(final boolean selectsFirstItem) {
    this.selectsFirstItem = selectsFirstItem;
  }

  public void setFocus(final boolean focused) {
    getTextBox().setFocus(focused);
  }

  /**
   * Sets the limit to the number of suggestions the oracle should provide. It is up to the oracle
   * to enforce this limit.
   * 
   * @param limit the limit to the number of suggestions provided
   */
  public void setLimit(final int limit) {
    this.limit = limit;
  }

  public void setTabIndex(final int index) {
    getTextBox().setTabIndex(index);
  }

  public void setText(final String text) {
    getTextBox().setText(text);
  }

  @Override
  public void setValue(final T newValue) {
    final String render = renderer.render(newValue);
    setText(render);
    currentValue = newValue;
    // /this.editor.setValue(newValue);
    // currentValue = newValue;
  }

  @Override
  public void setValue(final T value, final boolean fireEvents) {
    final String render = renderer.render(value);
    setText(render);
    currentValue = value;
  }

  /**
   * Show the current list of suggestions.
   */
  public void showSuggestionList() {
    if (isAttached()) {
      currentRenderedValue = null;
      refreshSuggestions();
    }
  }

  @Override
  protected void onEnsureDebugId(final String baseID) {
    super.onEnsureDebugId(baseID);
    display.onEnsureDebugId(baseID);
  }

  void showSuggestions(final String query) {
    if (query.length() == 0) {
      oracle.requestDefaultSuggestions(new ValueRequest(null, limit), callback);
    } else {
      oracle.requestSuggestions(new ValueRequest(query, limit), callback);
    }
  }

  private void addEventsToTextBox() {
    class TextBoxEvents extends HandlesAllKeyEvents implements ValueChangeHandler<String> {

      @Override
      public void onKeyDown(final KeyDownEvent event) {
        switch (event.getNativeKeyCode()) {
          case KeyCodes.KEY_DOWN:
            display.moveSelectionDown();
            break;
          case KeyCodes.KEY_UP:
            display.moveSelectionUp();
            break;
          case KeyCodes.KEY_ENTER:
          case KeyCodes.KEY_TAB:
            final ValueSuggestion<T> suggestion = display.getCurrentSelection();
            if (suggestion == null) {
              display.hideSuggestions();
            } else {
              setNewSelection(suggestion);
            }
            break;
        }
        delegateEvent(ValueSuggestBox.this, event);
      }

      @Override
      public void onKeyPress(final KeyPressEvent event) {
        delegateEvent(ValueSuggestBox.this, event);
      }

      @Override
      public void onKeyUp(final KeyUpEvent event) {
        // After every user key input, refresh the popup's suggestions.
        refreshSuggestions();
        delegateEvent(ValueSuggestBox.this, event);
      }

      @Override
      public void onValueChange(final ValueChangeEvent<String> event) {
        delegateEvent(ValueSuggestBox.this, event);
      }
    }

    final TextBoxEvents events = new TextBoxEvents();
    events.addKeyHandlersTo(getTextBox());
    getTextBox().addValueChangeHandler(events);
  }

  private void fireSuggestionEvent(final ValueSuggestion<T> selectedSuggestion) {
    SelectionEvent.fire(this, selectedSuggestion);
  }

  private void refreshSuggestions() {
    // Get the raw text.
    final String text = getText();
    if (text.equals(currentRenderedValue)) {
      return;
    } else {
      currentRenderedValue = text;
    }
    showSuggestions(text);
  }

  /**
   * Set the new suggestion in the text box.
   * 
   * @param curSuggestion the new suggestion
   */
  private void setNewSelection(final ValueSuggestion<T> curSuggestion) {
    assert curSuggestion != null : "suggestion cannot be null";
    final T replacementValue = curSuggestion.getReplacementValue();
    currentRenderedValue = renderer.render(replacementValue);
    setValue(replacementValue);
    display.hideSuggestions();
    fireSuggestionEvent(curSuggestion);
  }

  private static final String STYLENAME_DEFAULT = "gwt-SuggestBox";

}
