/*******************************************************************************
 * 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
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.edit.editor;

import java.util.List;

import org.eclipse.nebula.widgets.nattable.Messages;
import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.convert.ConversionFailedException;
import org.eclipse.nebula.widgets.nattable.data.convert.IDisplayConverter;
import org.eclipse.nebula.widgets.nattable.data.validate.IDataValidator;
import org.eclipse.nebula.widgets.nattable.data.validate.ValidationFailedException;
import org.eclipse.nebula.widgets.nattable.edit.EditConfigAttributes;
import org.eclipse.nebula.widgets.nattable.edit.EditConfigHelper;
import org.eclipse.nebula.widgets.nattable.edit.ICellEditHandler;
import org.eclipse.nebula.widgets.nattable.edit.command.EditSelectionCommand;
import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
import org.eclipse.nebula.widgets.nattable.style.CellStyleProxy;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.style.IStyle;
import org.eclipse.nebula.widgets.nattable.widget.EditModeEnum;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Abstract implementation of {@link ICellEditor} that wraps SWT controls to be
 * NatTable editors. It is used to hide several default behaviour and styling
 * from concrete editor implementations, so implementing an editor can focus on
 * the editor specific handling instead of NatTable default behaviour.
 * <p>
 * Note that most of the member variables defined will be set on activating the
 * editor. So you can not access those variables expecting reasonable values
 * prior activation. This makes it possible to use the same editor instance for
 * several cells instead of creating a new one for every cell.
 */
public abstract class AbstractCellEditor implements ICellEditor {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractCellEditor.class);

    /**
     * Flag indicating if the editor is closed or not.
     */
    private boolean closed;
    /**
     * The parent Composite, needed for the creation of the editor control. Used
     * internally for adding general behaviour, e.g. forcing the focus if the
     * editor is closed.
     */
    protected Composite parent;
    /**
     * The {@link ICellEditHandler} that will be used on commit.
     */
    private ICellEditHandler editHandler;
    /**
     * The style that should be used for rendering within the editor control.
     * Mainly it will cover foreground color, background color and font. If the
     * editor control supports further styles, this needs to be specified be the
     * ICellEditor implementation itself.
     */
    protected IStyle cellStyle;
    /**
     * The {@link IDisplayConverter} that should be used to convert the input
     * value to the canonical value and vice versa.
     */
    protected IDisplayConverter displayConverter;
    /**
     * The {@link IDataValidator} that should be used to validate the input
     * value prior committing.
     */
    protected IDataValidator dataValidator;
    /**
     * The {@link EditModeEnum} which is used to activate special behaviour and
     * styling. This is needed because activating an editor inline will have
     * different behaviour (e.g. moving the selection after commit) and styling
     * than rendering the editor on a subdialog.
     */
    protected EditModeEnum editMode;
    /**
     * The cell whose editor should be activated.
     */
    protected ILayerCell layerCell;
    /**
     * The {@link LabelStack} of the cell whose editor should be activated.
     */
    protected LabelStack labelStack;
    /**
     * The error handler that will be used to show conversion errors.
     */
    protected IEditErrorHandler conversionEditErrorHandler;
    /**
     * The error handler that will be used to show validation errors.
     */
    protected IEditErrorHandler validationEditErrorHandler;
    /**
     * The {@link IConfigRegistry} containing the configuration of the current
     * NatTable instance. This is necessary because the editors in the current
     * architecture are not aware of the NatTable instance they are running in.
     */
    protected IConfigRegistry configRegistry;

    /**
     * The {@link FocusListener} that will be added to the created editor
     * control for {@link EditModeEnum#INLINE} to close it if it loses focus.
     */
    protected FocusListener focusListener = new InlineFocusListener();

    /**
     * The {@link TraverseListener} that will be added to the created editor
     * control for {@link EditModeEnum#INLINE} trying to commit the editor prior
     * to traversal.
     */
    protected TraverseListener traverseListener = new InlineTraverseListener();

    @Override
    public final Control activateCell(Composite parent, Object originalCanonicalValue,
            EditModeEnum editMode, ICellEditHandler editHandler, ILayerCell cell,
            IConfigRegistry configRegistry) {

        this.closed = false;
        this.parent = parent;
        this.editHandler = editHandler;
        this.editMode = editMode;
        this.layerCell = cell;
        this.configRegistry = configRegistry;
        this.labelStack = cell.getConfigLabels();
        this.displayConverter = configRegistry.getConfigAttribute(
                CellConfigAttributes.DISPLAY_CONVERTER,
                DisplayMode.EDIT,
                this.labelStack);
        this.cellStyle = new CellStyleProxy(configRegistry, DisplayMode.EDIT, this.labelStack);
        this.dataValidator = configRegistry.getConfigAttribute(
                EditConfigAttributes.DATA_VALIDATOR,
                DisplayMode.EDIT,
                this.labelStack);

        this.conversionEditErrorHandler =
                EditConfigHelper.getEditErrorHandler(
                        configRegistry,
                        EditConfigAttributes.CONVERSION_ERROR_HANDLER,
                        this.labelStack);
        this.validationEditErrorHandler =
                EditConfigHelper.getEditErrorHandler(
                        configRegistry,
                        EditConfigAttributes.VALIDATION_ERROR_HANDLER,
                        this.labelStack);

        return activateCell(parent, originalCanonicalValue);
    }

    /**
     * This method will be called by
     * {@link AbstractCellEditor#activateCell(Composite, Object, EditModeEnum, ICellEditHandler, ILayerCell, IConfigRegistry)}
     * after initializing the activation values and before adding the default
     * listeners. In this method the underlying editor control should be created
     * and initialized, hiding default configuration from editor implementors.
     *
     * @param parent
     *            The parent Composite, needed for the creation of the editor
     *            control.
     * @param originalCanonicalValue
     *            The value that should be put to the activated editor control.
     * @return The SWT {@link Control} to be used for capturing the new cell
     *         value.
     */
    protected abstract Control activateCell(Composite parent, Object originalCanonicalValue);

    /**
     * @see ILayerCell#getColumnIndex()
     */
    @Override
    public int getColumnIndex() {
        return this.layerCell.getColumnIndex();
    }

    /**
     * @see ILayerCell#getRowIndex()
     */
    @Override
    public int getRowIndex() {
        return this.layerCell.getRowIndex();
    }

    /**
     * @see ILayerCell#getColumnPosition()
     */
    @Override
    public int getColumnPosition() {
        return this.layerCell.getColumnPosition();
    }

    /**
     * @see ILayerCell#getRowPosition()
     */
    @Override
    public int getRowPosition() {
        return this.layerCell.getRowPosition();
    }

    /**
     * Converts the current value in this editor using the configured
     * {@link IDisplayConverter}. If there is no {@link IDisplayConverter}
     * registered for this editor, the value itself will be returned.
     *
     * @return The canonical value after converting the current value or the
     *         value itself if no {@link IDisplayConverter} is configured.
     * @throws RuntimeException
     *             for conversion failures. As the {@link IDisplayConverter}
     *             interface does not specify throwing checked Exceptions on
     *             converting data, only unchecked Exceptions can occur. This is
     *             needed to stop further commit processing if the conversion
     *             failed.
     * @see IDisplayConverter
     */
    @Override
    public Object getCanonicalValue() {
        return getCanonicalValue(this.conversionEditErrorHandler);
    }

    /**
     * Converts the current value in this editor using the configured
     * {@link IDisplayConverter}. If there is no {@link IDisplayConverter}
     * registered for this editor, the value itself will be returned. Will use
     * the specified {@link IEditErrorHandler} for handling conversion errors.
     *
     * @param conversionErrorHandler
     *            The error handler that will be activated in case of conversion
     *            errors.
     * @return The canonical value after converting the current value or the
     *         value itself if no {@link IDisplayConverter} is configured.
     * @throws RuntimeException
     *             for conversion failures. As the {@link IDisplayConverter}
     *             interface does not specify throwing checked Exceptions on
     *             converting data, only unchecked Exceptions can occur. This is
     *             needed to stop further commit processing if the conversion
     *             failed.
     * @see IDisplayConverter
     */
    @Override
    public Object getCanonicalValue(IEditErrorHandler conversionErrorHandler) {
        return this.handleConversion(getEditorValue(), conversionErrorHandler);
    }

    /**
     * Converts the given display value using the configured
     * {@link IDisplayConverter}. If there is no {@link IDisplayConverter}
     * registered for this editor, the value itself will be returned. Will use
     * the specified {@link IEditErrorHandler} for handling conversion errors.
     *
     * @param displayValue
     *            The display value that needs to be converted.
     * @param conversionErrorHandler
     *            The error handler that will be activated in case of conversion
     *            errors.
     * @return The canonical value after converting the current value or the
     *         value itself if no {@link IDisplayConverter} is configured.
     * @throws RuntimeException
     *             for conversion failures. As the {@link IDisplayConverter}
     *             interface does not specify throwing checked Exceptions on
     *             converting data, only unchecked Exceptions can occur. This is
     *             needed to stop further commit processing if the conversion
     *             failed.
     * @see IDisplayConverter
     */
    protected Object handleConversion(Object displayValue, IEditErrorHandler conversionErrorHandler) {
        Object canonicalValue;
        try {
            if (this.displayConverter != null) {
                // always do the conversion to check for valid entered data
                canonicalValue = this.displayConverter.displayToCanonicalValue(this.layerCell, this.configRegistry, displayValue);
            } else {
                canonicalValue = displayValue;
            }

            // if the conversion succeeded, remove error rendering if exists
            conversionErrorHandler.removeError(this);
        } catch (ConversionFailedException e) {
            // conversion failed
            conversionErrorHandler.displayError(this, this.configRegistry, e);
            throw e;
        } catch (Exception e) {
            // conversion failed
            conversionErrorHandler.displayError(this, this.configRegistry, e);
            throw new ConversionFailedException(e.getMessage(), e);
        }
        return canonicalValue;
    }

    @Override
    public void setCanonicalValue(Object canonicalValue) {
        Object displayValue;
        if (this.displayConverter != null) {
            displayValue = this.displayConverter.canonicalToDisplayValue(this.layerCell, this.configRegistry, canonicalValue);
        } else {
            displayValue = canonicalValue;
        }
        setEditorValue(displayValue);
    }

    @Override
    public boolean validateCanonicalValue(Object canonicalValue) {
        return validateCanonicalValue(canonicalValue, this.validationEditErrorHandler);
    }

    @Override
    public boolean validateCanonicalValue(Object canonicalValue, IEditErrorHandler validationEditErrorHandler) {
        // do the validation if a validator is registered
        if (this.dataValidator != null) {
            try {
                boolean validationResult = this.dataValidator.validate(this.layerCell, this.configRegistry, canonicalValue);

                // if the validation succeeded, remove error rendering if exists
                if (validationResult) {
                    validationEditErrorHandler.removeError(this);
                } else {
                    throw new ValidationFailedException(Messages.getString("AbstractCellEditor.validationFailure")); //$NON-NLS-1$
                }
                return validationResult;
            } catch (Exception e) {
                // validation failed
                validationEditErrorHandler.displayError(this, this.configRegistry, e);
                return false;
            }
        }

        return true;
    }

    @Override
    public boolean commit(MoveDirectionEnum direction) {
        return commit(direction, true);
    }

    @Override
    public boolean commit(MoveDirectionEnum direction, boolean closeAfterCommit) {
        return commit(direction, closeAfterCommit, false);
    }

    @Override
    public boolean commit(MoveDirectionEnum direction, boolean closeAfterCommit, boolean skipValidation) {
        if (this.editHandler != null && !this.closed) {
            try {
                // always do the conversion
                Object canonicalValue = getCanonicalValue();
                if (skipValidation || (!skipValidation && validateCanonicalValue(canonicalValue))) {
                    boolean committed = this.editHandler.commit(canonicalValue, direction);

                    if (committed && closeAfterCommit) {
                        close();

                        if (direction != MoveDirectionEnum.NONE && (openAdjacentEditor() || activateOnTraversal(this.configRegistry, this.labelStack))) {
                            this.layerCell.getLayer().doCommand(
                                    new EditSelectionCommand(this.parent, this.configRegistry, true));
                        }
                    }

                    return committed;
                }
            } catch (ConversionFailedException | ValidationFailedException e) {
                // do nothing as exceptions caused by conversion/validation are
                // handled already we just need this catch block for stopping
                // the process if conversion failed with an exception
            } catch (Exception e) {
                // if another exception occured that wasn't thrown by us, it
                // should at least be logged without killing the whole
                // application
                LOG.error("Error on updating cell value: {}", e.getLocalizedMessage(), e); //$NON-NLS-1$
            }
        }
        return false;
    }

    @Override
    public void close() {
        this.closed = true;
        if (this.parent != null && !this.parent.isDisposed()) {
            this.parent.forceFocus();
        }

        removeEditorControlListeners();

        Control editorControl = getEditorControl();
        if (editorControl != null && !editorControl.isDisposed()) {
            editorControl.dispose();
        }
    }

    @Override
    public boolean isClosed() {
        return this.closed;
    }

    @Override
    public boolean openInline(IConfigRegistry configRegistry, List<String> configLabels) {
        return EditConfigHelper.openInline(configRegistry, configLabels);
    }

    @Override
    public boolean supportMultiEdit(IConfigRegistry configRegistry, List<String> configLabels) {
        return EditConfigHelper.supportMultiEdit(configRegistry, configLabels);
    }

    @Override
    public boolean openMultiEditDialog() {
        return true;
    }

    @Override
    public boolean openAdjacentEditor() {
        return EditConfigHelper.openAdjacentEditor(this.configRegistry, this.labelStack);
    }

    @Override
    public boolean activateAtAnyPosition() {
        return true;
    }

    @Override
    public boolean activateOnTraversal(IConfigRegistry configRegistry, List<String> configLabels) {
        return EditConfigHelper.activateEditorOnTraversal(configRegistry, configLabels);
    }

    @Override
    public void addEditorControlListeners() {
        Control editorControl = getEditorControl();
        if (editorControl != null && !editorControl.isDisposed() && this.editMode == EditModeEnum.INLINE) {
            // only add the focus and traverse listeners for inline mode
            editorControl.addFocusListener(this.focusListener);
            editorControl.addTraverseListener(this.traverseListener);
        }
    }

    @Override
    public void removeEditorControlListeners() {
        Control editorControl = getEditorControl();
        if (editorControl != null && !editorControl.isDisposed()) {
            editorControl.removeFocusListener(this.focusListener);
            editorControl.removeTraverseListener(this.traverseListener);
        }
    }

    @Override
    public Rectangle calculateControlBounds(Rectangle cellBounds) {
        return cellBounds;
    }

    /**
     * This method can be used to set the {@link IDataValidator} to use. This
     * might be useful e.g. the configured validator needs to be wrapped to add
     * special behaviour. Setting a validator prior to activating the editor
     * will have no effect.
     * <p>
     * Note: It is not suggested to call this method in custom code. It is used
     * e.g. by the TickUpdateCellEditDialog as dependent on the selected update
     * type, the validator needs to be enabled or not.
     * </p>
     *
     * @param validator
     *            The {@link IDataValidator} to set.
     */
    public void setDataValidator(IDataValidator validator) {
        this.dataValidator = validator;
    }

    /**
     * {@link FocusListener} that tries to commit if the focus is lost.
     * Otherwise it tries to keep the focus in the editor.
     *
     * @since 1.4
     */
    protected class InlineFocusListener extends FocusAdapter {

        public boolean handleFocusChanges = true;

        @Override
        public void focusLost(FocusEvent e) {
            if (this.handleFocusChanges) {
                if (!commit(MoveDirectionEnum.NONE, true)) {
                    if (!e.widget.isDisposed() && e.widget instanceof Control) {
                        ((Control) e.widget).forceFocus();
                    }
                } else {
                    if (!AbstractCellEditor.this.parent.isDisposed()) {
                        AbstractCellEditor.this.parent.forceFocus();
                    }
                }
            }
        }
    }

    /**
     * {@link TraverseListener} that will try to commit and close this editor
     * with the current value, prior to proceed the traversal. If the commit
     * fails and the editor can not be closed, the traversal will not be
     * processed.
     *
     * @since 1.4
     */
    protected class InlineTraverseListener implements TraverseListener {
        @Override
        public void keyTraversed(TraverseEvent event) {
            boolean committed = false;
            if (event.keyCode == SWT.TAB && event.stateMask == SWT.MOD2) {
                committed = commit(MoveDirectionEnum.LEFT);
            } else if (event.keyCode == SWT.TAB && event.stateMask == 0) {
                committed = commit(MoveDirectionEnum.RIGHT);
            }
            if (!committed) {
                event.doit = false;
            }
        }
    }
}
