/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 453898
 *     Ryan McHale <rpmc22@gmail.com> - Bug 484716
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.edit.editor;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.edit.EditConstants;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.nebula.widgets.nattable.widget.EditModeEnum;
import org.eclipse.nebula.widgets.nattable.widget.NatCombo;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.ShellAdapter;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Text;

/**
 * {@link ICellEditor} implementation to provide combo box editing behaviour.
 * Uses the {@link NatCombo} as editor control which provides free editing in
 * the text control part and multi selection in the dropdown part if configured.
 * <p>
 * You can create a ComboBoxCellEditor either by setting the items to show
 * statically by constructor, or by using an {@link IComboBoxDataProvider}. Last
 * one is a way to dynamically populate the items showed in a combobox in
 * NatTable. It is not possible to mix these two approaches!
 *
 */
public class ComboBoxCellEditor extends AbstractCellEditor {

    /**
     * The wrapped editor control.
     */
    private NatCombo combo;

    /**
     * The maximum number of items the drop down will show before introducing a
     * scroll bar.
     */
    protected int maxVisibleItems;

    /**
     * The list of canonical values that will be set as selectable items to the
     * combo. If this {@link ComboBoxCellEditor} is created for using such a
     * list, the selectable values will be static.
     * <p>
     * The values will be converted to the corresponding display values prior
     * filling the combo.
     * <p>
     * If this {@link ComboBoxCellEditor} is created for using a
     * {@link IComboBoxDataProvider} this list will be ignored.
     */
    private List<?> canonicalValues;

    /**
     * The {@link IComboBoxDataProvider} that is used to set the selectable
     * items to the combo. If this {@link ComboBoxCellEditor} is created using
     * such a data provider, the selectable value will be dynamic.
     * <p>
     * The values will be converted to the corresponding display values prior
     * filling the combo.
     * <p>
     * If a {@link IComboBoxDataProvider} is set, a possible set static list of
     * canonical values will be ignored.
     */
    private IComboBoxDataProvider dataProvider;

    /**
     * Flag that indicates whether a text box is displayed to filter the drop
     * down options
     *
     * @since 1.4
     */
    protected boolean showDropdownFilter = false;

    /**
     * Flag that indicates whether this ComboBoxCellEditor supports free editing
     * in the text control of the NatCombo or not. By default free editing is
     * disabled.
     */
    protected boolean freeEdit = false;

    /**
     * Flag that indicates whether this ComboBoxCellEditor supports multiple
     * selection or not. By default multiple selection is disabled.
     */
    protected boolean multiselect = false;

    /**
     * Flag that indicates whether this ComboBoxCellEditor shows checkboxes for
     * items in the dropdown or not.
     */
    protected boolean useCheckbox = false;

    /**
     * String that is used to separate values in the String representation
     * showed in the text control if multiselect is supported. <code>null</code>
     * to use the default String ", ".
     */
    protected String multiselectValueSeparator = null;

    /**
     * String that is used to prefix the generated String representation showed
     * in the text control if multiselect is supported. Needed to visualize the
     * multiselection to the user. If this value is <code>null</code> the
     * default String "[" is used.
     */
    protected String multiselectTextPrefix = null;

    /**
     * String that is used to suffix the generated String representation showed
     * in the text control if multiselect is supported. Needed to visualize the
     * multiselection to the user. If this value is <code>null</code> the
     * default String "]" is used.
     */
    protected String multiselectTextSuffix = null;

    /**
     * The image to use as overlay to the {@link Text} Control if the dropdown
     * is visible. It will indicate that the control is an open combo to the
     * user. If this value is <code>null</code> the default image specified in
     * NatCombo will be used.
     */
    protected Image iconImage;

    /**
     * The list of the canonical values that are currently shown in the opened
     * NatCombo. Needed in case of multi selection and dynamic changing content
     * via data provider.
     */
    private List<?> currentCanonicalValues;

    /**
     * Flag to configure whether the focus should be set to the text field or
     * the dropdown of the combobox.
     *
     * @since 1.6
     */
    private boolean focusOnText = false;

    /**
     * Create a new single selection {@link ComboBoxCellEditor} based on the
     * given list of items, showing the default number of items in the dropdown
     * of the combo.
     *
     * @param canonicalValues
     *            Array of items to be shown in the drop down box. These will be
     *            converted using the {@link IDisplayConverter} for display
     *            purposes
     */
    public ComboBoxCellEditor(List<?> canonicalValues) {
        this(canonicalValues, NatCombo.DEFAULT_NUM_OF_VISIBLE_ITEMS);
    }

    /**
     * Create a new single selection {@link ComboBoxCellEditor} based on the
     * given list of items, showing the default number of items in the dropdown
     * of the combo.
     *
     * @param canonicalValues
     *            Array of items to be shown in the drop down box. These will be
     *            converted using the {@link IDisplayConverter} for display
     *            purposes
     *
     * @since 1.5
     */
    public ComboBoxCellEditor(Object... canonicalValues) {
        this(Arrays.asList(canonicalValues), NatCombo.DEFAULT_NUM_OF_VISIBLE_ITEMS);
    }

    /**
     * Create a new single selection {@link ComboBoxCellEditor} based on the
     * given list of items.
     *
     * @param canonicalValues
     *            Array of items to be shown in the drop down box. These will be
     *            converted using the {@link IDisplayConverter} for display
     *            purposes
     * @param maxVisibleItems
     *            The maximum number of items the drop down will show before
     *            introducing a scroll bar.
     */
    public ComboBoxCellEditor(List<?> canonicalValues, int maxVisibleItems) {
        this.canonicalValues = canonicalValues;
        this.maxVisibleItems = maxVisibleItems;
    }

    /**
     * Create a new single selection {@link ComboBoxCellEditor} based on the
     * given {@link IComboBoxDataProvider}, showing the default number of items
     * in the dropdown of the combo.
     *
     * @param dataProvider
     *            The {@link IComboBoxDataProvider} that is responsible for
     *            populating the items to the dropdown box. This is the way to
     *            use a ComboBoxCellEditor with dynamic content.
     */
    public ComboBoxCellEditor(IComboBoxDataProvider dataProvider) {
        this(dataProvider, NatCombo.DEFAULT_NUM_OF_VISIBLE_ITEMS);
    }

    /**
     * Create a new single selection {@link ComboBoxCellEditor} based on the
     * given {@link IComboBoxDataProvider}.
     *
     * @param dataProvider
     *            The {@link IComboBoxDataProvider} that is responsible for
     *            populating the items to the dropdown box. This is the way to
     *            use a ComboBoxCellEditor with dynamic content.
     * @param maxVisibleItems
     *            The maximum number of items the drop down will show before
     *            introducing a scroll bar.
     */
    public ComboBoxCellEditor(IComboBoxDataProvider dataProvider, int maxVisibleItems) {
        this.dataProvider = dataProvider;
        this.maxVisibleItems = maxVisibleItems;
    }

    @Override
    protected Control activateCell(Composite parent, final Object originalCanonicalValue) {
        this.combo = createEditorControl(parent);

        // filling and populating a multiselect combo could take some time for
        // huge data sets
        BusyIndicator.showWhile(parent.getDisplay(), () -> {
            fillCombo();

            setCanonicalValue(originalCanonicalValue);
        });

        // open the dropdown immediately after the Text control of the NatCombo
        // is positioned
        if (this.editMode == EditModeEnum.INLINE) {
            final boolean focusOnText = (originalCanonicalValue instanceof Character || this.focusOnText);

            this.combo.addTextControlListener(new ControlAdapter() {
                @Override
                public void controlResized(ControlEvent e) {
                    ComboBoxCellEditor.this.combo.showDropdownControl(focusOnText);
                    ComboBoxCellEditor.this.combo.removeTextControlListener(this);
                }

                @Override
                public void controlMoved(ControlEvent e) {
                    ComboBoxCellEditor.this.combo.showDropdownControl(focusOnText);
                    ComboBoxCellEditor.this.combo.removeTextControlListener(this);
                }
            });
        }

        return this.combo;
    }

    /**
     * This implementation overrides the default implementation because we can
     * work on the list of canonical items in the combo directly. Only for
     * multiselect in combination with free editing, we need to convert here
     * ourself.
     */
    @Override
    public Object getCanonicalValue() {
        if (!this.multiselect) {
            // single selection handling
            int selectionIndex = this.combo.getSelectionIndex();

            // Item selected from list
            if (selectionIndex >= 0) {
                if (this.dataProvider != null) {
                    return this.dataProvider.getValues(getColumnIndex(), getRowIndex()).get(selectionIndex);
                } else {
                    return this.canonicalValues.get(selectionIndex);
                }
            } else {
                // if there is no selection in the dropdown, we need to check if
                // there is a free edit in the NatCombo control
                if (this.combo.getSelection().length > 0) {
                    return super.getCanonicalValue();
                }
            }
        } else {
            // multi selection handling
            int[] selectionIndices = this.combo.getSelectionIndices();

            // as we are in multiselection mode, we always return a Collection
            // and never null
            List<Object> result = null;

            // Item selected from list
            if (selectionIndices.length > 0) {
                result = new ArrayList<>(selectionIndices.length);
                for (int i : selectionIndices) {
                    result.add(this.currentCanonicalValues.get(i));
                }
            } else {
                // if there is no selection in the dropdown, we need to check if
                // there is a free edit in the NatCombo control
                String[] comboSelection = this.combo.getSelection();
                if (comboSelection.length > 0) {
                    result = new ArrayList<>(comboSelection.length);
                    for (String selection : comboSelection) {
                        result.add(handleConversion(selection, this.conversionEditErrorHandler));
                    }
                }
            }

            // if nothing is selected and there is no free edit, we return an
            // empty Collection
            return result != null ? result : new ArrayList<>();
        }

        return null;
    }

    /**
     * This implementation overrides the default implementation because of the
     * special handling for comboboxes. It can handle multi selection and needs
     * to transfer the converted values into a String array so the values in the
     * combobox can be selected.
     *
     * @param canonicalValue
     *            The canonical value to be set to the wrapped editor control.
     */
    @Override
    public void setCanonicalValue(Object canonicalValue) {
        if (canonicalValue != null) {
            String[] editorValues = null;
            if (canonicalValue instanceof List<?>) {
                List<?> temp = (List<?>) canonicalValue;
                if (this.displayConverter != null) {
                    editorValues = temp.stream()
                            .map(canonical -> (String) this.displayConverter.canonicalToDisplayValue(
                                    this.layerCell,
                                    this.configRegistry,
                                    canonical))
                            .toArray(size -> new String[size]);
                } else {
                    editorValues = temp.stream()
                            .map(canonical -> canonical != null ? canonical.toString() : "") //$NON-NLS-1$
                            .toArray(size -> new String[size]);
                }
            } else {
                // in case the SELECT_ALL value is set for selecting all values
                // in the combo we don't need a conversion and use the value
                if (EditConstants.SELECT_ALL_ITEMS_VALUE.equals(canonicalValue)
                        || this.displayConverter == null) {
                    editorValues = new String[] { canonicalValue.toString() };
                } else {
                    editorValues = new String[] {
                            (String) this.displayConverter.canonicalToDisplayValue(
                                    this.layerCell,
                                    this.configRegistry,
                                    canonicalValue) };
                }
            }
            setEditorValue(editorValues);
        }
    }

    /**
     * Will set the items selectable in the combo dependent on the configuration
     * of this {@link ComboBoxCellEditor}. As the combo is only able to handle
     * Strings in the combo itself, and this editor works directly on the
     * canonical values, the values are converted in here too.
     */
    private void fillCombo() {
        List<String> displayValues = new ArrayList<>();

        if (this.dataProvider != null) {
            this.currentCanonicalValues = this.dataProvider.getValues(getColumnIndex(), getRowIndex());
        } else {
            this.currentCanonicalValues = this.canonicalValues;
        }

        for (Object canonicalValue : this.currentCanonicalValues) {
            Object displayValue = this.displayConverter != null
                    ? this.displayConverter.canonicalToDisplayValue(
                            this.layerCell,
                            this.configRegistry,
                            canonicalValue)
                    : canonicalValue;
            displayValues.add(displayValue != null ? displayValue.toString() : ""); //$NON-NLS-1$
        }

        this.combo.setItems(displayValues.toArray(ArrayUtil.STRING_TYPE_ARRAY));
    }

    @Override
    public void close() {
        super.close();
        this.currentCanonicalValues = null;
    }

    @Override
    public Object getEditorValue() {
        if (!this.multiselect) {
            return this.combo.getSelection()[0];
        }
        return this.combo.getSelection();
    }

    @Override
    public void setEditorValue(Object value) {
        this.combo.setSelection((String[]) value);
    }

    @Override
    public NatCombo getEditorControl() {
        return this.combo;
    }

    @Override
    public NatCombo createEditorControl(Composite parent) {
        int style = SWT.NONE;
        if (!this.freeEdit) {
            style |= SWT.READ_ONLY;
        }
        if (this.multiselect) {
            style |= SWT.MULTI;
        }
        if (this.useCheckbox) {
            style |= SWT.CHECK;
        }
        final NatCombo combo = (this.iconImage == null)
                ? new NatCombo(parent, this.cellStyle, this.maxVisibleItems, style, this.showDropdownFilter)
                : new NatCombo(parent, this.cellStyle, this.maxVisibleItems, style, this.iconImage, this.showDropdownFilter);

        combo.setCursor(new Cursor(Display.getDefault(), SWT.CURSOR_IBEAM));

        if (this.multiselect) {
            combo.setMultiselectValueSeparator(this.multiselectValueSeparator);
            combo.setMultiselectTextBracket(this.multiselectTextPrefix, this.multiselectTextSuffix);
        }

        addNatComboListener(combo);
        return combo;
    }

    /**
     * Registers special listeners to the {@link NatCombo} regarding the
     * {@link EditModeEnum}, that are needed to commit/close or change the
     * visibility state of the {@link NatCombo} dependent on UI interactions.
     *
     * @param combo
     *            The {@link NatCombo} to add the listeners to.
     */
    protected void addNatComboListener(final NatCombo combo) {
        combo.addKeyListener(new KeyAdapter() {

            @Override
            public void keyPressed(KeyEvent event) {
                if ((event.keyCode == SWT.CR)
                        || (event.keyCode == SWT.KEYPAD_CR)) {
                    commit(MoveDirectionEnum.NONE, ComboBoxCellEditor.this.editMode == EditModeEnum.INLINE);
                } else if (event.keyCode == SWT.ESC) {
                    if (ComboBoxCellEditor.this.editMode == EditModeEnum.INLINE) {
                        close();
                    } else {
                        combo.hideDropdownControl();
                    }
                }
            }

        });

        combo.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseUp(MouseEvent e) {
                commit(MoveDirectionEnum.NONE,
                        (!ComboBoxCellEditor.this.multiselect && ComboBoxCellEditor.this.editMode == EditModeEnum.INLINE));
                if (!ComboBoxCellEditor.this.multiselect && ComboBoxCellEditor.this.editMode == EditModeEnum.DIALOG) {
                    // hide the dropdown after a value was selected in the combo
                    // in a dialog
                    combo.hideDropdownControl();
                }
            }
        });

        if (this.editMode == EditModeEnum.INLINE) {
            combo.addShellListener(new ShellAdapter() {
                @Override
                public void shellClosed(ShellEvent e) {
                    close();
                }
            });
        }

        if (this.editMode == EditModeEnum.DIALOG) {
            combo.addFocusListener(new FocusAdapter() {
                @Override
                public void focusLost(FocusEvent e) {
                    combo.hideDropdownControl();
                }
            });
        }
    }

    /**
     * Selects the item at the given zero-relative index in the receiver's list.
     * If the item at the index was already selected, it remains selected.
     * Indices that are out of range are ignored.
     *
     * @param index
     *            the index of the item to select
     *
     * @see org.eclipse.swt.widgets.List#select(int)
     */
    public void select(int index) {
        this.combo.select(index);
    }

    /**
     * Selects the items at the given zero-relative indices in the receiver. The
     * current selection is not cleared before the new items are selected.
     * <p>
     * If the item at a given index is not selected, it is selected. If the item
     * at a given index was already selected, it remains selected. Indices that
     * are out of range and duplicate indices are ignored. If the receiver is
     * single-select and multiple indices are specified, then all indices are
     * ignored.
     *
     * @param indices
     *            the array of indices for the items to select
     *
     * @see org.eclipse.swt.widgets.List#select(int[])
     */
    public void select(int[] indices) {
        this.combo.select(indices);
    }

    /**
     * @param multiselectValueSeparator
     *            String that should be used to separate values in the String
     *            representation showed in the text control if multiselect is
     *            supported. <code>null</code> to use the default value
     *            separator.
     * @see NatCombo#DEFAULT_MULTI_SELECT_VALUE_SEPARATOR
     */
    public void setMultiselectValueSeparator(String multiselectValueSeparator) {
        this.multiselectValueSeparator = multiselectValueSeparator;
    }

    /**
     * Set the prefix and suffix that will parenthesize the text that is created
     * out of the selected values if this NatCombo supports multiselection.
     *
     * @param multiselectTextPrefix
     *            String that should be used to prefix the generated String
     *            representation showed in the text control if multiselect is
     *            supported. <code>null</code> to use the default prefix.
     * @param multiselectTextSuffix
     *            String that should be used to suffix the generated String
     *            representation showed in the text control if multiselect is
     *            supported. <code>null</code> to use the default suffix.
     * @see NatCombo#DEFAULT_MULTI_SELECT_PREFIX
     * @see NatCombo#DEFAULT_MULTI_SELECT_SUFFIX
     */
    public void setMultiselectTextBracket(String multiselectTextPrefix, String multiselectTextSuffix) {
        this.multiselectTextPrefix = multiselectTextPrefix;
        this.multiselectTextSuffix = multiselectTextSuffix;
    }

    /**
     * @return The image that is used as overlay to the {@link Text} Control if
     *         the dropdown is visible. It will indicate that the control is an
     *         open combo to the user. If this value is <code>null</code> the
     *         default image specified in NatCombo will be used.
     */
    public Image getIconImage() {
        return this.iconImage;
    }

    /**
     * @param iconImage
     *            The image to use as overlay to the {@link Text} Control if the
     *            dropdown is visible. It will indicate that the control is an
     *            open combo to the user. If this value is <code>null</code> the
     *            default image specified in NatCombo will be used.
     */
    public void setIconImage(Image iconImage) {
        this.iconImage = iconImage;
    }

    /**
     * @return <code>true</code> if this ComboBoxCellEditor supports free
     *         editing in the text control of the NatCombo or not. By default
     *         free editing is disabled.
     */
    public boolean isFreeEdit() {
        return this.freeEdit;
    }

    /**
     * @param freeEdit
     *            <code>true</code> to indicate that this ComboBoxCellEditor
     *            supports free editing in the text control of the NatCombo,
     *            <code>false</code> if not.
     */
    public void setFreeEdit(boolean freeEdit) {
        this.freeEdit = freeEdit;
    }

    /**
     * @return <code>true</code> if this ComboBoxCellEditor supports multiple
     *         selection or not. By default multiple selection is disabled.
     */
    public boolean isMultiselect() {
        return this.multiselect;
    }

    /**
     * @param multiselect
     *            <code>true</code> to indicate that this ComboBoxCellEditor
     *            supports multiple selection, <code>false</code> if not.
     */
    public void setMultiselect(boolean multiselect) {
        this.multiselect = multiselect;
    }

    /**
     * @return <code>true</code> if this ComboBoxCellEditor shows checkboxes for
     *         items in the dropdown. By default there are not checkboxes shown.
     */
    public boolean isUseCheckbox() {
        return this.useCheckbox;
    }

    /**
     * @param useCheckbox
     *            <code>true</code> if this ComboBoxCellEditor should show
     *            checkboxes for items in the dropdown, <code>false</code> if
     *            not.
     */
    public void setUseCheckbox(boolean useCheckbox) {
        this.useCheckbox = useCheckbox;
    }

    /**
     * @return <code>true</code> if this ComboBoxCellEditor should show the text
     *         control for filtering items in the dropdown. By default the
     *         filter is not shown.
     *
     * @since 1.4
     */
    public boolean isShowDropdownFilter() {
        return this.showDropdownFilter;
    }

    /**
     * @param showDropdownFilter
     *            <code>true</code> if this ComboBoxCellEditor should show the
     *            text control for filtering items in the dropdown,
     *            <code>false</code> if not.
     *
     * @since 1.4
     */
    public void setShowDropdownFilter(boolean showDropdownFilter) {
        this.showDropdownFilter = showDropdownFilter;
    }

    /**
     *
     * @return <code>true</code> if the focus on the activated combobox is set
     *         to the text field, <code>false</code> if the dropdown gets the
     *         focus. Default is <code>false</code>.
     *
     * @since 1.6
     */
    public boolean isFocusOnText() {
        return this.focusOnText;
    }

    /**
     *
     * @param focusOnText
     *            <code>true</code> if the focus on the activated combobox
     *            should be set to the text field, <code>false</code> if the
     *            dropdown should get the focus.
     *
     * @since 1.6
     */
    public void setFocusOnText(boolean focusOnText) {
        this.focusOnText = focusOnText;
    }
}
