/*******************************************************************************
 * Copyright (c) 2012-2017 Codenvy, S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *   Codenvy, S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.che.ide.ui.listbox;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.LabelElement;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.HasChangeHandlers;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.FocusWidget;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.RootPanel;

/**
 * Custom list box widget for use instead of com.google.gwt.user.client.ui.ListBox which based on select tag.
 *
 * @author Oleksii Orel
 */
public class CustomListBox extends FocusWidget implements HasChangeHandlers {

    private static final CustomListBoxResources RESOURCES = GWT.create(CustomListBoxResources.class);

    static {
        RESOURCES.getCSS().ensureInjected();
    }

    private final LabelElement currentItemLabel = Document.get().createLabelElement();
    private final FlowPanel    optionsPanel     = new FlowPanel();
    private final String       optionsGroupName = "listBox-" + Document.get().createUniqueId();

    private int     dropDownSize         = 6;
    private int     selectedIndex        = -1;
    private int     defaultSelectedIndex = -1;
    private boolean isActive             = false;

    public static CustomListBox wrap(Element element) {
        // Assert that the element is attached.
        assert Document.get().getBody().isOrHasChild(element);

        CustomListBox customListBox = new CustomListBox(element);

        // Mark it attached and remember it for cleanup.
        customListBox.onAttach();
        RootPanel.detachOnWindowClose(customListBox);

        return customListBox;
    }

    /**
     * Creates an empty custom list box.
     */
    public CustomListBox() {
        super(Document.get().createDivElement());

        Element listBoxElement = getElement();

        listBoxElement.appendChild(currentItemLabel);
        listBoxElement.appendChild(optionsPanel.getElement());
        listBoxElement.appendChild(RESOURCES.arrow().getSvg().getElement());

        optionsPanel.setVisible(false);

        addDomHandler(new BlurHandler() {
            @Override
            public void onBlur(BlurEvent event) {
                optionsPanel.setVisible(false);
                isActive = false;
            }
        }, BlurEvent.getType());

        addDomHandler(new MouseDownHandler() {
            @Override
            public void onMouseDown(MouseDownEvent event) {
                //Update isActive state. It actually when we lose onBlur event in the parent widget.
                isActive = isActive(getElement());
                if (!isActive) {
                    optionsPanel.setVisible(true);
                }
            }
        }, MouseDownEvent.getType());

        addDomHandler(new MouseUpHandler() {
            @Override
            public void onMouseUp(MouseUpEvent event) {
                if (isActive) {
                    optionsPanel.setVisible(!optionsPanel.isVisible());
                } else {
                    isActive = true;
                }
            }
        }, MouseUpEvent.getType());

        addChangeHandler(new ChangeHandler() {
            @Override
            public void onChange(ChangeEvent event) {
                selectedIndex = -1;
                NodeList<Element> selectionElements = optionsPanel.getElement().getElementsByTagName("input");
                for (int pos = 0; pos < selectionElements.getLength(); pos++) {
                    InputElement inputElement = (InputElement)selectionElements.getItem(pos);
                    if (inputElement.isChecked()) {
                        //update currentItemLabel
                        currentItemLabel.setInnerText(getItemText(pos));
                        selectedIndex = pos;
                        break;
                    }
                }
                if (selectedIndex == -1) {
                    currentItemLabel.setInnerText("");
                }
            }
        });

        setStyleName(RESOURCES.getCSS().listBox());
    }

    /**
     * This constructor may be used by subclasses to explicitly use an existing element.
     *
     * @param element
     *         the element to be used
     */
    protected CustomListBox(Element element) {
        super(element);
    }

    private InputElement getListItemElement(int index) {
        final Element optionElement = (Element)optionsPanel.getElement().getChild(index);

        return (InputElement)optionElement.getElementsByTagName("input").getItem(0);
    }

    /**
     * Adds an ChangeHandler.
     *
     * @param handler
     *         the change handler
     */
    public HandlerRegistration addChangeHandler(ChangeHandler handler) {
        return addDomHandler(handler, ChangeEvent.getType());
    }

    /**
     * Adds an item to the list box.
     *
     * @param item
     *         the text of the item to be added
     */
    public void addItem(String item) {
        this.insertItem(item);
    }

    /**
     * Adds an item to the list box, specifying an initial value for the item.
     *
     * @param item
     *         the text of the item to be added
     * @param value
     *         the item's value, to be submitted if it is part of a
     *         {@link com.google.gwt.user.client.ui.FormPanel}; cannot be <code>null</code>
     */
    public void addItem(String item, String value) {
        this.insertItem(item, value);
    }

    /**
     * Removes all items from the list box.
     */
    public void clear() {
        selectedIndex = -1;
        currentItemLabel.setInnerText("");
        optionsPanel.getElement().removeAllChildren();
    }

    /**
     * Gets the number of items present in the list box.
     *
     * @return the number of items
     */
    public int getItemCount() {
        return optionsPanel.getElement().getChildCount();
    }

    /**
     * Gets the text associated with the item at the specified index.
     *
     * @param index
     *         the index of the item whose text is to be retrieved
     * @return the text associated with the item
     * @throws IndexOutOfBoundsException
     *         if the index is out of range
     */
    public String getItemText(int index) {
        checkIndex(index);
        final Element optionElement = (Element)optionsPanel.getElement().getChild(index);
        final LabelElement labelElement = (LabelElement)optionElement.getElementsByTagName("label").getItem(0);

        return labelElement.getInnerText();
    }

    /**
     * Gets the text for currently selected item. If multiple items are selected,
     * this method will return the text of the first selected item.
     *
     * @return the text for selected item, or {@code null} if none is selected
     */
    public String getSelectedItemText() {
        int index = getSelectedIndex();

        return index == -1 ? null : getItemText(index);
    }

    /**
     * Gets the currently-selected item.
     *
     * @return the selected index, or <code>-1</code> if none is selected
     */
    public int getSelectedIndex() {
        return selectedIndex;
    }

    /**
     * Gets the value associated with the item at a given index.
     *
     * @param index
     *         the index of the item to be retrieved
     * @return the item's associated value
     * @throws IndexOutOfBoundsException
     *         if the index is out of range
     */
    public String getValue(int index) {
        checkIndex(index);
        final Element optionElement = (Element)optionsPanel.getElement().getChild(index);
        final InputElement inputElement = (InputElement)optionElement.getElementsByTagName("input").getItem(0);

        return inputElement.getValue();
    }

    /**
     * Gets the value for currently selected item.
     *
     * @return the value for selected item, or {@code null} if none is selected
     */
    public String getValue() {
        int index = getSelectedIndex();

        return index == -1 ? null : getValue(index);
    }

    /**
     * Inserts an item into the custom list box.
     *
     * @param item
     *         the text of the item to be inserted
     */
    public void insertItem(String item) {
        this.insertItem(item, item);
    }

    /**
     * Inserts an item into the list box.
     *
     * @param item
     *         the text of the item to be inserted.
     * @param value
     *         the item's value.
     */
    public void insertItem(String item, String value) {
        //create new widget
        final RadioButton radioButton = new RadioButton(optionsGroupName, item);
        //remove the default gwt-RadioButton style
        radioButton.removeStyleName("gwt-RadioButton");
        //set value
        final InputElement inputElement = (InputElement)radioButton.getElement().getElementsByTagName("input").getItem(0);
        inputElement.removeAttribute("tabindex");
        inputElement.setAttribute("value", value);
        //set default state
        if (defaultSelectedIndex > -1 && optionsPanel.getElement().getChildCount() == defaultSelectedIndex) {
            inputElement.setChecked(true);
            currentItemLabel.setInnerText(item);
        }
        //add to widget
        optionsPanel.add(radioButton);
    }

    /**
     * Sets custom height inside widget as height and line-height properties.
     *
     * @param height
     */
    public void setHeight(String height) {
        this.getElement().getStyle().setProperty("height", height);
        currentItemLabel.getStyle().setProperty("lineHeight", height);
        optionsPanel.getElement().getStyle().setProperty("lineHeight", height);
        optionsPanel.getElement().getStyle().setProperty("maxHeight", "calc(" + dropDownSize + "*" + height + ")");
    }

    /**
     * Sets dropdown part size.
     *
     * @param dropDownSize
     * @throws IndexOutOfBoundsException
     *         if the index is out of range
     */
    public void setSize(int dropDownSize) {
        if (dropDownSize < 1 || dropDownSize > 99) {
            throw new IndexOutOfBoundsException();
        }
        this.dropDownSize = dropDownSize;
    }

    /**
     * Determines whether an individual list item is selected.
     *
     * @param index
     *         the index of the item to be tested
     * @return <code>true</code> if the item is selected
     * @throws IndexOutOfBoundsException
     *         if the index is out of range
     */
    public boolean isItemSelected(int index) {
        checkIndex(index);

        return selectedIndex == index;
    }

    /**
     * Removes the item at the specified index.
     *
     * @param index
     *         the index of the item to be removed
     * @throws IndexOutOfBoundsException
     *         if the index is out of range
     */
    public void removeItem(int index) {
        checkIndex(index);
        if (index == selectedIndex) {
            currentItemLabel.setInnerText("");
            selectedIndex = -1;
        }
        optionsPanel.getElement().removeChild(optionsPanel.getElement().getChild(index));
    }

    /**
     * Sets whether an individual list item is selected.
     *
     * @param index
     *         the index of the item to be selected or unselected
     * @param selected
     *         <code>true</code> to select the item
     */
    public void setItemSelected(int index, boolean selected) {
        if (index < 0 || index >= getItemCount()) {
            return;
        }
        if (selected) {
            selectedIndex = index;
            currentItemLabel.setInnerText(getItemText(index));
        }
        final InputElement inputElement = getListItemElement(index);
        inputElement.setChecked(selected);
    }

    /**
     * Sets the text associated with the item at a given index.
     *
     * @param index
     *         the index of the item to be set
     * @param text
     *         the item's new text
     * @throws IndexOutOfBoundsException
     *         if the index is out of range
     */
    public void setItemText(int index, String text) {
        checkIndex(index);
        final Element optionElement = (Element)optionsPanel.getElement().getChild(index);
        final LabelElement labelElement = (LabelElement)optionElement.getElementsByTagName("label").getItem(0);
        labelElement.setInnerText(text);
        if (selectedIndex == index) {
            currentItemLabel.setInnerText(text);
        }
    }

    /**
     * Sets the currently selected index.
     *
     * @param index
     *         the index of the item to be selected
     */
    public void setSelectedIndex(int index) {
        if (index < 0) {
            return;
        }

        //set default index if not added options yet
        if (index >= getItemCount()) {
            defaultSelectedIndex = index;
            return;
        }

        selectedIndex = index;
        currentItemLabel.setInnerText(getItemText(index));
        InputElement inputElement = getListItemElement(index);
        inputElement.setChecked(true);
    }

    /**
     * Selects an item with given text.
     *
     * @param text
     *          text of an item to be selected
     */
    public void select(String text) {
        // uncheck previous value
        if (selectedIndex >= 0) {
            InputElement inputElement = getListItemElement(selectedIndex);
            inputElement.setChecked(false);
        }

        // find and select a new one
        if (text != null) {
            for (int i = 0; i < getItemCount(); i++) {
                if (text.equals(getItemText(i))) {
                    setSelectedIndex(i);
                    return;
                }
            }
        }

        // clear the selection
        selectedIndex = -1;
        currentItemLabel.setInnerText("");
    }

    /**
     * Sets the value associated with the item at a given index.
     *
     * @param index
     *         the index of the item to be set
     * @param value
     *         the item's new value
     * @throws IndexOutOfBoundsException
     *         if the index is out of range
     */
    public void setValue(int index, String value) {
        checkIndex(index);
        final InputElement inputElement = getListItemElement(index);
        inputElement.setValue(value);
    }

    /**
     * @see com.google.gwt.user.client.ui.UIObject#onEnsureDebugId(String)
     */
    @Override
    protected void onEnsureDebugId(String baseID) {
        super.onEnsureDebugId(baseID);
    }

    private void checkIndex(int index) {
        if (index < 0 || index >= getItemCount()) {
            throw new IndexOutOfBoundsException();
        }
    }

    /**
     * Check isActive status.
     */
    private native boolean isActive(Element element) /*-{
        var activeElement = $doc.activeElement;
        return activeElement.isEqualNode(element);
    }-*/;

}
