/*
 * Copyright 2011 Sergey Skladchikov
 *
 * 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 consulo.web.gwt.client.ui.advancedGwt;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.EventTarget;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.ToggleButton;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConnection;
import org.gwt.advanced.client.datamodel.ListDataModel;
import org.gwt.advanced.client.datamodel.ListModelEvent;
import org.gwt.advanced.client.datamodel.ListModelListener;
import org.gwt.advanced.client.ui.widget.combo.ComboBoxChangeEvent;
import org.gwt.advanced.client.ui.widget.combo.DefaultListItemFactory;
import org.gwt.advanced.client.ui.widget.combo.DropDownPosition;
import org.gwt.advanced.client.ui.widget.combo.ListItemFactory;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * By VISTALL. This is copy-paste version of {@link org.gwt.advanced.client.ui.widget.ComboBox}  with change:
 * - Current item is Widget too as List Items (in original current item is TextEditor),
 * this need for show current item as in list, not only text variant
 * <p>
 * <p>
 * This is a combo box widget implementation.
 *
 * @author <a href="mailto:sskladchikov@gmail.com">Sergey Skladchikov</a>
 * @since 1.2.0
 */
public class WidgetComboBox extends WidgetButtonPanel implements HasAllFocusHandlers, HasAllKeyHandlers, HasClickHandlers, ListModelListener, HasChangeHandlers {
  /**
   * a combo box data model
   */
  private ListDataModel model;
  /**
   * a list item factory
   */
  private ListItemFactory listItemFactory;
  /**
   * a list popup panel
   */
  private ListPopupPanel2 listPanel;
  /**
   * a combo box delegate listener
   */
  private DelegateHandler delegateHandler;
  /**
   * a keyboard events listener that switches off default browser handling and replaces it with conponents'
   */
  private ComboBoxKeyboardManager keyboardManager;
  /**
   * a flag that is <code>true</code> if any control key is pressed
   */
  private boolean keyPressed;

  private ApplicationConnection myConnection;

  /**
   * Setter for property 'listItemFactory'.
   *
   * @param listItemFactory Value to set for property 'listItemFactory'.
   */
  public void setListItemFactory(ListItemFactory listItemFactory) {
    if (listItemFactory != null) this.listItemFactory = listItemFactory;
    if (isListPanelOpened()) getListPanel().prepareList();
  }

  @Override
  public HandlerRegistration addBlurHandler(BlurHandler handler) {
    return addHandler(handler, BlurEvent.getType());
  }

  @Override
  public HandlerRegistration addFocusHandler(FocusHandler handler) {
    return addHandler(handler, FocusEvent.getType());
  }

  @Override
  public HandlerRegistration addKeyUpHandler(KeyUpHandler handler) {
    return addHandler(handler, KeyUpEvent.getType());
  }

  @Override
  public HandlerRegistration addKeyDownHandler(KeyDownHandler handler) {
    return addHandler(handler, KeyDownEvent.getType());
  }

  @Override
  public HandlerRegistration addKeyPressHandler(KeyPressHandler handler) {
    return addHandler(handler, KeyPressEvent.getType());
  }

  /**
   * Note that handlers added by this method will receive
   * {@link org.gwt.advanced.client.ui.widget.combo.ComboBoxChangeEvent}s.
   *
   * @param handler the change handler
   * @return a handler registration.
   */
  @Override
  public HandlerRegistration addChangeHandler(ChangeHandler handler) {
    return addHandler(handler, ChangeEvent.getType());
  }

  @Override
  public HandlerRegistration addClickHandler(ClickHandler handler) {
    return addHandler(handler, ClickEvent.getType());
  }

  /**
   * Getter for property 'model'.
   *
   * @return Value for property 'model'.
   */
  public ListDataModel getModel() {
    if (model == null) {
      model = new ComboBoxDataModel2();
      model.addListModelListener(this);
    }
    return model;
  }

  /**
   * Getter for property 'listItemFactory'.
   *
   * @return Value for property 'listItemFactory'.
   */
  public ListItemFactory getListItemFactory() {
    if (listItemFactory == null) listItemFactory = new DefaultListItemFactory();
    return listItemFactory;
  }

  /**
   * This method sets focus on this widget.<p/>
   * But note that the combo box is not a focus event sourcer. It siply delegtes this functionality
   * to the text box.
   *
   * @param focus is a flag of focus.
   */
  public void setFocus(boolean focus) {
    //if (isCustomTextAllowed())
    //  getSelectedValue().setFocus(focus);
    // else
    getChoiceButton().setFocus(focus);
  }

  /**
   * This method check the list panel status.
   *
   * @return <code>true</code> if it's opened.
   */
  public boolean isListPanelOpened() {
    return !getListPanel().isHidden();
  }

  /**
   * This method returns a value currently displayed in the text box.
   *
   * @return a text value.
   */
  //public String getText() {
  //  return getSelectedValue().getText();
  //}

  /**
   * Gets a number of visible rows.<p/>
   * Values <= 0 interpreted as undefined.
   *
   * @return a visible rows to be displayed without scrolling.
   */
  public int getVisibleRows() {
    return getListPanel().getVisibleRows();
  }

  /**
   * Sets visible rows number.<p/>
   * You can pass a value <= 0. It will mean that this parameter in undefined.
   *
   * @param visibleRows is a number of rows to be displayed without scrolling.
   */
  public void setVisibleRows(int visibleRows) {
    getListPanel().setVisibleRows(visibleRows);
  }

  /**
   * Sets an item index that must be displayed on top.<p/>
   * If the item is outside the currently visible area the list will be scrolled down
   * to this item.
   *
   * @param index is an index of the item that must be displayed on top of the visible area.
   */
  public void setStartItemIndex(int index) {
    getListPanel().setStartItemIndex(index);
  }

  public int getStartItemIndex() {
    return getListPanel().getStartItemIndex();
  }

  /**
   * Sets text to the selected value box but doesn't change anything
   * in the list of items.
   *
   * @param text is a text to set.
   */
  //public void setText(String text) {
  //  getSelectedValue().setText(text);
  //}


  /**
   * This method returns a selected item.
   *
   * @return is a selected item.
   */
  public Object getSelected() {
    return getModel().getSelected();
  }

  /**
   * This method returns a selected item index.
   *
   * @return is a selected item index.
   */
  public int getSelectedIndex() {
    return getModel().getSelectedIndex();
  }

  /**
   * This method returns a selected item ID.
   *
   * @return is a selected item ID.
   */
  public String getSelectedId() {
    return getModel().getSelectedId();
  }

  /**
   * This method sets the selected item ID.
   *
   * @param id is an item ID to select.
   */
  public void setSelectedId(String id) {
    getModel().setSelectedId(id);
  }

  /**
   * This method sets the selected item index.
   *
   * @param index a selected item index.
   */
  public void setSelectedIndex(int index) {
    getModel().setSelectedIndex(index);
  }

  /**
   * Opens / closes list popup panel by request.
   *
   * @param opened <code>true</code> means "show".
   */
  public void setListPopupOpened(boolean opened) {
    if (opened) {
      getListPanel().show();
    }
    else {
      getListPanel().hide();
    }
  }

  /**
   * This method gets a widget that is currently selected in the drop down list.
   *
   * @return <code>null</code> if the drop down list is collapsed.
   */
  public Widget getSelectedWidget() {
    if (isListPanelOpened() && getModel().getSelectedIndex() >= 0) {
      FlowPanel list = getListPanel().getList();
      if (list.getWidgetCount() > getModel().getSelectedIndex()) return list.getWidget(getModel().getSelectedIndex());
      return null;
    }
    else {
      return null;
    }
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void cleanSelection() {
    super.cleanSelection();
    getModel().clear();
  }

  /**
   * Gets a highlight row number.<p/>
   * Note that sometimes this value is not equal to the selected row.
   *
   * @return a highlight row number or <code>-1</code> if nothing is highlight.
   */
  public int getHighlightRow() {
    return getListPanel().getHighlightRow();
  }

  /**
   * Sets a highlight row number and display the row as selected but not actually
   * select it.
   *
   * @param row is a row number to highlight. If it's out of range thus method does nothing.
   */
  public void setHighlightRow(int row) {
    getListPanel().setHighlightRow(row);
  }

  /**
   * This method gets an actual number of items displayed in the drop down.
   *
   * @return an item count.
   */
  public int getItemCount() {
    return getListPanel().getItemCount();
  }

  /**
   * Gets an item by its index<p/>
   * If index < 0 or index >= {@link #getItemCount()} it throws an exception.
   *
   * @param index is an index of the item to get.
   * @return a foudn item.
   * @throws IndexOutOfBoundsException if index is invalid.
   */
  public Widget getItem(int index) {
    return getListPanel().getItem(index);
  }

  /**
   * Gets an item index if it's displayed in the drop down list.<p/>
   * Otherwise returns <code>-1</code>.
   *
   * @param item an item that is required to return.
   * @return an item index value or <code>-1</code>.
   */
  public int getItemIndex(Widget item) {
    return getListPanel().getItemIndex(item);
  }

  /**
   * This method shows the drop down list.
   *
   * @param prepareList forces the list to be prepared (refreshed) before displaying.
   */
  public void showList(boolean prepareList) {
    getListPanel().show();
    if (prepareList) getListPanel().prepareList();
    if (getItemCount() <= 0) getListPanel().hide();
  }

  /**
   * Moves the cursor up or down.
   *
   * @param step is a number of items relative to the current cursor position.
   */
  public void moveCursor(int step) {
    int row = getListPanel().getHighlightRow();
    if (step == 0 || row + step < 0 || row + step >= getItemCount()) return;

    row += step;

    if (row != getListPanel().getHighlightRow()) {
      if (row >= getModel().getCount()) row = getModel().getCount() - 1;
      if (row < 0) row = 0;

      getListPanel().setHighlightRow(row);
      Widget item = getListPanel().getList().getWidget(row);
      getListPanel().ensureVisible(item);
    }
  }

  /**
   * Hides the drop down list.
   */
  public void hideList() {
    getListPanel().hide();
    getChoiceButton().setDown(false);
  }

  /**
   * Selects the specified item in the model and in the drop down list.
   *
   * @param row is a row index to select
   */
  public void select(int row) {
    getModel().setSelectedIndex(row);
    getListPanel().hide();
    getSelectedValue().removeStyleName("selected-row");
    getChoiceButton().setDown(false);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public void onModelEvent(ListModelEvent event) {
    if (event.getType() == ListModelEvent.ADD_ITEM) {
      add(event);

      // refresh current item
      prepareSelectedValue();
    }
    else if (event.getType() == ListModelEvent.CLEAN) {
      clean(event);

      // refresh current item
      prepareSelectedValue();
    }
    else if (event.getType() == ListModelEvent.REMOVE_ITEM) {
      remove(event);

      // refresh current item
      prepareSelectedValue();
    }
    else if (event.getType() == ListModelEvent.SELECT_ITEM) {
      select(event);
    }

    getListPanel().adjustSize();
  }

  /**
   * Checks whether the lazy rendering option is enabled.
   *
   * @return a result of check.
   */
  public boolean isLazyRenderingEnabled() {
    return getListPanel().isLazyRenderingEnabled();
  }

  /**
   * Enables or disables lazy rendering option.<p/>
   * If this option is enabled the widget displays only several items on lazily renders other ones on scroll down.<p/>
   * By default lazy rendering is disabled. Switch it on for really large (over 500 items) lists only.<p/>
   * Note that <i>lazy rendering</i> is not <i>lazy data loading</i>. The second one means that the data is loaded into
   * the model on request where as the first option assumes that all necessary data has already been loaded and put
   * into the model. If you need <i>lazy loading</i> please consider using {@link org.gwt.advanced.client.ui.widget.SuggestionBox} and
   * {@link org.gwt.advanced.client.datamodel.SuggestionBoxDataModel}.
   *
   * @param lazyRenderingEnabled is an option value.
   */
  public void setLazyRenderingEnabled(boolean lazyRenderingEnabled) {
    getListPanel().setLazyRenderingEnabled(lazyRenderingEnabled);
  }

  /**
   * Gets applied position of the drop down list.
   *
   * @return a drop down list position value.
   */
  public DropDownPosition getDropDownPosition() {
    return getListPanel().getDropDownPosition();
  }

  /**
   * Sets applied position of the drop down list.<p/>
   * Being set the drop down is immediately applied if the list is opened.
   *
   * @param dropDownPosition is a drop down list position value.
   */
  public void setDropDownPosition(DropDownPosition dropDownPosition) {
    getListPanel().setDropDownPosition(dropDownPosition);
  }

  /**
   * Adds a new visual item into the drop down list every time when it's added into the data
   * model.
   *
   * @param event is an event containing data about the item.
   */
  protected void add(ListModelEvent event) {
    if (isListPanelOpened()) {
      for (Map.Entry<String, Integer> entry : event.getItemIndexes().entrySet()) {
        if (entry.getValue() <= getItemCount()) {
          Widget item = getListItemFactory().createWidget(event.getSource().get(entry.getKey()));
          item = getListPanel().adoptItemWidget(item);

          if (entry.getValue() < getListPanel().getList().getWidgetCount()) {
            getListPanel().getList().insert(item, entry.getValue());
          }
          else {
            getListPanel().getList().add(item);
          }
        }
        else {
          getListPanel().prepareList();
          if (getItemCount() <= 0) getListPanel().hide();
        }
      }
    }
  }

  /**
   * This method cleans the drop down list on each data clean.
   *
   * @param event is a clean event.
   */
  @SuppressWarnings({"UnusedDeclaration"})
  protected void clean(ListModelEvent event) {
    if (isListPanelOpened()) {
      getListPanel().getList().clear();
      getListPanel().hide();
    }
  }

  /**
   * Removes a visual item from the drop down list if the remove event is received.
   *
   * @param event is an event that contains data of the removed item.
   */
  protected void remove(ListModelEvent event) {
    if (isListPanelOpened()) {
      for (Map.Entry<String, Integer> entry : event.getItemIndexes().entrySet()) {
        if (entry.getValue() < getListPanel().getList().getWidgetCount()) {
          getListPanel().remove(getListPanel().getList().getWidget(entry.getValue()));
          if (getListPanel().getList().getWidgetCount() <= 0) getListPanel().hide();
        }
      }
    }
  }

  /**
   * Highlights the visual item in the drop down list if it's selected in the model.
   *
   * @param event is an event that contains data about selected item.
   */
  protected void select(ListModelEvent event) {
    if (isListPanelOpened()) {
      getListPanel().setHighlightRow(event.getItemIndex());
    }

    final Widget widget = getListItemFactory().createWidget(model.getSelected());
    final SimplePanel selectedValue = getSelectedValue();
    selectedValue.setWidget(widget);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected String getDefaultImageName() {
    return "drop-down.gif";
  }

  /**
   * Returns <code>true</code> if cursor is moved by a control key.
   *
   * @return a flag value.
   */
  protected boolean isKeyPressed() {
    return keyPressed;
  }

  /**
   * Sets the value of the key pressed flag.
   *
   * @param keyPressed is a key pressed flag value.
   */
  protected void setKeyPressed(boolean keyPressed) {
    this.keyPressed = keyPressed;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void prepareSelectedValue() {
    super.prepareSelectedValue();
    final Widget widget = getListItemFactory().createWidget(getModel().getSelected());
    getSelectedValue().setWidget(widget);
  }

  /**
   * {@inheritDoc}
   */
  @Override
  protected void addComponentListeners() {
    ComboBoxSelectItem value = getSelectedValue();
    ToggleButton button = getChoiceButton();

    getListPanel().addChangeHandler(getDelegateHandler());

    //value.addChangeHandler(getDelegateHandler());
    button.addFocusHandler(getDelegateHandler());
    value.addFocusHandler(getDelegateHandler());
    button.addBlurHandler(getDelegateHandler());
    value.addBlurHandler(getDelegateHandler());
    value.addClickHandler(getDelegateHandler());
    button.addClickHandler(getDelegateHandler());
    value.addKeyUpHandler(getDelegateHandler());
    value.addKeyDownHandler(getDelegateHandler());
    value.addKeyPressHandler(getDelegateHandler());
  }


  /**
   * Getter for property 'listPanel'.
   *
   * @return Value for property 'listPanel'.
   */
  protected ListPopupPanel2 getListPanel() {
    if (listPanel == null) {
      listPanel = new ListPopupPanel2(this) {
        @Override
        protected ApplicationConnection getApplicationConnection() {
          return myConnection;
        }
      };
    }
    return listPanel;
  }

  /**
   * Getter for property 'delegateHandler'.
   *
   * @return Value for property 'delegateHandler'.
   */
  protected DelegateHandler getDelegateHandler() {
    if (delegateHandler == null) delegateHandler = new DelegateHandler();
    return delegateHandler;
  }

  /**
   * This method gets a keybord manager implementation for the component.
   *
   * @return an instance of the manager.
   */
  protected ComboBoxKeyboardManager getKeyboardManager() {
    if (keyboardManager == null) keyboardManager = new ComboBoxKeyboardManager();
    return keyboardManager;
  }

  /**
   * Universal handler that delegates all events handling to custom handlers.
   */
  protected class DelegateHandler implements FocusHandler, BlurHandler, ClickHandler, ChangeHandler, KeyUpHandler, KeyDownHandler, KeyPressHandler {
    /**
     * a list of focused controls
     */
    private Set<Object> focuses;
    /**
     * keyboard manager handler registration
     */
    private HandlerRegistration keyboardManagerRegistration;

    @Override
    public void onBlur(BlurEvent event) {
      if (!isFocus()) return;

      if (keyboardManagerRegistration != null) {
        keyboardManagerRegistration.removeHandler();
        keyboardManagerRegistration = null;
      }

      Object sender = event.getSource();
      getFocuses().remove(sender);

      SimplePanel value = getSelectedValue();
      if (sender == value && !false) value.removeStyleName("selected-row");

      if (!isFocus()) fireEvent(event);
    }

    @Override
    public void onFocus(FocusEvent event) {
      Object sender = event.getSource();
      getFocuses().add(sender);

      if (keyboardManagerRegistration == null) keyboardManagerRegistration = Event.addNativePreviewHandler(getKeyboardManager());

      SimplePanel value = getSelectedValue();
      if (sender == value) {
        value.addStyleName("selected-row");
        if (isChoiceButtonVisible()) getChoiceButton().setFocus(true);
      }
      else if (sender == null || sender == getListPanel()) { //on drop down list show
        Widget widget = getSelectedWidget();
        if (widget != null) getListPanel().ensureVisible(widget);
      }

      if (focuses.size() == 1) fireEvent(event);
    }

    @Override
    public void onChange(ChangeEvent event) {
      if (event.getSource() == getListPanel()) {
        //getSelectedValue().setText(getListItemFactory().convert(getModel().getSelected()));
        getListPanel().hide();
        getSelectedValue().removeStyleName("selected-row");
        getChoiceButton().setDown(false);
      }
      fireEvent(event);
    }

    @Override
    public void onClick(ClickEvent event) {
      int count = getModel().getCount();
      if (count > 0 && !getListPanel().isShowing()) {
        getListPanel().show();
        getListPanel().prepareList();
        if (getItemCount() <= 0) getListPanel().hide();
        getChoiceButton().setDown(true);
      }
      else {
        getListPanel().hide();
        getChoiceButton().setDown(false);
      }
      fireEvent(event);
    }

    @Override
    public void onKeyUp(KeyUpEvent event) {
      fireEvent(event);
    }

    @Override
    public void onKeyPress(KeyPressEvent event) {
      fireEvent(event);
    }

    @Override
    public void onKeyDown(KeyDownEvent event) {
      fireEvent(event);
    }

    protected Set<Object> getFocuses() {
      if (focuses == null) focuses = new HashSet<Object>();
      return focuses;
    }

    /**
     * Getter for property 'focus'.
     *
     * @return Value for property 'focus'.
     */
    protected boolean isFocus() {
      return getFocuses().size() > 0;
    }
  }

  /**
   * This is a keyboard manager implementation developed for the widget.<p/>
   * It prevents default browser event handling for system keys like arrow up / down, escape, enter and tab.
   * This manager is activated on widget focus and is used for opening / closing the drop down list and
   * switching a cursor position in the list.<p/>
   * It also supports Shift+Tab combination but skips other modifiers.
   */
  protected class ComboBoxKeyboardManager implements Event.NativePreviewHandler {
    /**
     * See class docs
     */
    @Override
    public void onPreviewNativeEvent(Event.NativePreviewEvent event) {
      NativeEvent nativeEvent = event.getNativeEvent();
      EventTarget eventTarget = nativeEvent.getEventTarget();
      if (!Element.is(eventTarget)) //chrome fix
      {
        return;
      }

      Element target = (Element)Element.as(eventTarget);

      int type = event.getTypeInt();
      if (type == Event.ONKEYDOWN) {
        setKeyPressed(true);
        if (DOM.getCaptureElement() != null) return;

        boolean eventTargetsPopup = (target != null) && DOM.isOrHasChild(getElement(), target);
        int button = nativeEvent.getKeyCode();
        boolean alt = nativeEvent.getAltKey();
        boolean ctrl = nativeEvent.getCtrlKey();
        boolean shift = nativeEvent.getShiftKey();

        boolean hasModifiers = alt || ctrl || shift;

        if (eventTargetsPopup && isListPanelOpened()) {
          if (button == KeyCodes.KEY_UP && !hasModifiers) {
            moveCursor(-1);
            cancelAndPrevent(event);
          }
          else if (button == KeyCodes.KEY_DOWN && !hasModifiers) {
            moveCursor(1);
            cancelAndPrevent(event);
          }
          else if (button == KeyCodes.KEY_ENTER && !hasModifiers) {
            select(getHighlightRow());
            getChoiceButton().setFocus(false);
            ChangeEvent changeEvent = new ComboBoxChangeEvent(getHighlightRow(), ComboBoxChangeEvent.ChangeEventInputDevice.KEYBOARD);
            fireEvent(changeEvent);
            setKeyPressed(false);
          }
          else if (button == KeyCodes.KEY_ESCAPE && !hasModifiers) {
            hideList();
            setKeyPressed(false);
          }
          else if (button == KeyCodes.KEY_TAB && (!hasModifiers || !alt && !ctrl && shift)) {
            hideList();
            setKeyPressed(false);
          }
        }
        else if (eventTargetsPopup && !hasModifiers && button == KeyCodes.KEY_ENTER && getModel().getCount() > 0) {
          showList(true);
        }
      }
      else if (type == Event.ONKEYUP) {
        setKeyPressed(false);
      }
    }

    /**
     * This method cancels and prevents default actions of the specified event.<p/>
     * It's useful for stupid browsers like Opera.
     *
     * @param event is an event to cancel and prevent.
     */
    protected void cancelAndPrevent(Event.NativePreviewEvent event) {
      event.getNativeEvent().preventDefault();
      event.cancel();
    }
  }

  public void setConnection(ApplicationConnection connection) {
    myConnection = connection;
  }

  public ApplicationConnection getConnection() {
    return myConnection;
  }
}
