/*******************************************************************************
 * 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.config;

import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.nebula.widgets.nattable.Messages;
import org.eclipse.nebula.widgets.nattable.data.convert.ConversionFailedException;
import org.eclipse.nebula.widgets.nattable.data.validate.ValidationFailedException;
import org.eclipse.nebula.widgets.nattable.edit.editor.AbstractEditErrorHandler;
import org.eclipse.nebula.widgets.nattable.edit.editor.ICellEditor;
import org.eclipse.nebula.widgets.nattable.edit.editor.IEditErrorHandler;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer.MoveDirectionEnum;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;

/**
 * Strategy class for conversion/validation failures. If the entered value is
 * not valid, a warning dialog with the corresponding error message will show
 * up. The warning dialog gives the opportunity to discard the invalid input or
 * change it, which will cause the editor to stay open. Only handles errors on
 * commit.
 */
public class DialogErrorHandling extends AbstractEditErrorHandler {

    /**
     * The {@link ICellEditor} for which this {@link DialogErrorHandling} is
     * activated. Needed so it is possible to operate on it dependent on the
     * users choice.
     */
    protected ICellEditor editor;
    /**
     * Flag to configure if this dialog allows to commit invalid data. This is
     * necessary to implement cross validation in NatTable by using dialogs to
     * tell the user what is wrong. By default this value is set to
     * <code>false</code> as cross validation is not the default validation use
     * case.
     */
    protected boolean allowCommit = false;
    /**
     * The shell title that will be used if there is no conversion or validation
     * shell title configured.
     */
    private String failureShellTitle = "%DialogErrorHandlingStrategy.failureTitle"; //$NON-NLS-1$
    /**
     * The shell title that will be used in case this
     * {@link DialogErrorHandling} is called to handle a
     * {@link ConversionFailedException}.
     */
    private String conversionFailureShellTitle = "%DialogErrorHandlingStrategy.conversionFailureTitle"; //$NON-NLS-1$
    /**
     * The shell title that will be used in case this
     * {@link DialogErrorHandling} is called to handle a
     * {@link ValidationFailedException}.
     */
    private String validationFailureShellTitle = "%DialogErrorHandlingStrategy.validationFailureTitle"; //$NON-NLS-1$
    /**
     * The text on the button for changing the entered value.
     */
    private String changeButtonLabel = "%DialogErrorHandlingStrategy.warningDialog.changeButton"; //$NON-NLS-1$
    /**
     * The text on the button to discard the entered value.
     */
    private String discardButtonLabel = "%DialogErrorHandlingStrategy.warningDialog.discardButton"; //$NON-NLS-1$
    /**
     * The text on the button to commit the entered invalid value. Needed to
     * support cross validation.
     */
    private String commitButtonLabel = "%DialogErrorHandlingStrategy.warningDialog.commitButton"; //$NON-NLS-1$

    /**
     * Create a new {@link DialogErrorHandling} with no underlying
     * {@link IEditErrorHandler} that does not support cross validation.
     */
    public DialogErrorHandling() {
        this(null, false);
    }

    /**
     * Create a new {@link DialogErrorHandling} with no underlying
     * {@link IEditErrorHandler} that gives the opportunity to configure the
     * error handling for cross validation.
     *
     * @param allowCommit
     *            Flag to configure if this dialog allows to commit invalid
     *            data. If this parameter is set to <code>true</code>, an
     *            additional button for committing the invalid data will be
     *            provided within the dialog.
     */
    public DialogErrorHandling(boolean allowCommit) {
        this(null, allowCommit);

    }

    /**
     * Create a new {@link DialogErrorHandling} using the given
     * {@link IEditErrorHandler} as the underlying to allow chaining of error
     * handling. Using this constructor there is no cross validation support.
     *
     * @param underlyingErrorHandler
     *            The underlying {@link IEditErrorHandler}
     */
    public DialogErrorHandling(IEditErrorHandler underlyingErrorHandler) {
        this(underlyingErrorHandler, false);
    }

    /**
     * Create a new {@link DialogErrorHandling} using the given
     * {@link IEditErrorHandler} as the underlying to allow chaining of error
     * handling.
     *
     * @param underlyingErrorHandler
     *            The underlying {@link IEditErrorHandler}
     * @param allowCommit
     *            Flag to configure if this dialog allows to commit invalid
     *            data. If this parameter is set to <code>true</code>, an
     *            additional button for committing the invalid data will be
     *            provided within the dialog.
     */
    public DialogErrorHandling(IEditErrorHandler underlyingErrorHandler, boolean allowCommit) {
        super(underlyingErrorHandler);
        this.allowCommit = allowCommit;
    }

    /**
     * {@inheritDoc} After the error is handled by its underlying
     * {@link IEditErrorHandler}, a dialog will be opened showing the error
     * message to the user, giving the opportunity to decide if the entered
     * value should be discarded or if the editor should stay open so the value
     * can be modified.
     */
    @Override
    public void displayError(ICellEditor cellEditor, Exception e) {
        super.displayError(cellEditor, e);
        this.editor = cellEditor;

        String shellTitle = getFailureShellTitle();
        if (e instanceof ConversionFailedException
                && !getConversionFailureShellTitle().isEmpty()) {
            shellTitle = getConversionFailureShellTitle();
        } else if (e instanceof ValidationFailedException
                && !getValidationFailureShellTitle().isEmpty()) {
            shellTitle = getValidationFailureShellTitle();
        }
        showWarningDialog(e.getLocalizedMessage(), shellTitle);
    }

    /**
     * Shows a warning dialog if the conversion or the validation returned an
     * error message. Otherwise nothing happens.
     *
     * @param dialogMessage
     *            the dialog message
     * @param dialogTitle
     *            the dialog title
     */
    protected void showWarningDialog(String dialogMessage, String dialogTitle) {
        if (!isWarningDialogActive() && dialogMessage != null) {
            // conversion/validation failed - so open dialog with error message

            String[] buttonLabels = this.allowCommit ? new String[] { getChangeButtonLabel(), getDiscardButtonLabel(), getCommitButtonLabel() }
                    : new String[] { getChangeButtonLabel(), getDiscardButtonLabel() };

            MessageDialog warningDialog = new MessageDialog(
                    Display.getDefault().getActiveShell(),
                    dialogTitle,
                    null,
                    dialogMessage,
                    MessageDialog.WARNING,
                    buttonLabels,
                    0);

            // if discard was selected close the editor
            int returnCode = warningDialog.open();
            if (returnCode == 1) {
                this.editor.close();
            }
            // if commit was selected, commit the value by skipping the
            // validation
            else if (returnCode == 2) {
                this.editor.commit(MoveDirectionEnum.NONE, true, true);
            }
        }
    }

    /**
     * Checks if the current active Shell is a conversion/validation failure
     * warning dialog. As a Shell has not id it is checked if the Shell title is
     * for conversion or validation failure in localized format.
     *
     * @return <code>true</code> if a warning dialog is active
     */
    protected boolean isWarningDialogActive() {
        // check if the current active shell is a conversion or validation
        // failure warning dialog
        Shell control = Display.getDefault().getActiveShell();
        return (control != null
                && (getConversionFailureShellTitle().equals(control.getText())
                        || getValidationFailureShellTitle().equals(control.getText())
                        || getFailureShellTitle().equals(control.getText())));
    }

    /**
     * @return The shell title that will be used if there is no conversion or
     *         validation shell title configured.
     */
    public String getFailureShellTitle() {
        return getLocalized(this.failureShellTitle);
    }

    /**
     * @param failureShellTitle
     *            The shell title that should be used if there is no conversion
     *            or validation shell title configured.
     */
    public void setFailureShellTitle(String failureShellTitle) {
        this.failureShellTitle = failureShellTitle;
    }

    /**
     * @return The shell title that will be used in case this
     *         {@link DialogErrorHandling} is called to handle a
     *         {@link ConversionFailedException}.
     */
    public String getConversionFailureShellTitle() {
        return getLocalized(this.conversionFailureShellTitle);
    }

    /**
     * @param conversionFailureShellTitle
     *            The shell title that should be used in case this
     *            {@link DialogErrorHandling} is called to handle a
     *            {@link ConversionFailedException}.
     */
    public void setConversionFailureShellTitle(String conversionFailureShellTitle) {
        this.conversionFailureShellTitle = conversionFailureShellTitle;
    }

    /**
     * @return The shell title that will be used in case this
     *         {@link DialogErrorHandling} is called to handle a
     *         {@link ValidationFailedException}.
     */
    public String getValidationFailureShellTitle() {
        return getLocalized(this.validationFailureShellTitle);
    }

    /**
     * @param validationFailureShellTitle
     *            The shell title that should be used in case this
     *            {@link DialogErrorHandling} is called to handle a
     *            {@link ValidationFailedException}.
     */
    public void setValidationFailureShellTitle(String validationFailureShellTitle) {
        this.validationFailureShellTitle = validationFailureShellTitle;
    }

    /**
     * @return The text on the button for changing the entered value.
     */
    public String getChangeButtonLabel() {
        return getLocalized(this.changeButtonLabel);
    }

    /**
     * @param changeButtonLabel
     *            The text on the button for changing the entered value.
     */
    public void setChangeButtonLabel(String changeButtonLabel) {
        this.changeButtonLabel = changeButtonLabel;
    }

    /**
     * @return The text on the button to discard the entered value.
     */
    public String getDiscardButtonLabel() {
        return getLocalized(this.discardButtonLabel);
    }

    /**
     * @param discardButtonLabel
     *            The text on the button to discard the entered value.
     */
    public void setDiscardButtonLabel(String discardButtonLabel) {
        this.discardButtonLabel = discardButtonLabel;
    }

    /**
     * @return The text on the button to commit the entered value.
     * @since 1.4
     */
    public String getCommitButtonLabel() {
        return getLocalized(this.commitButtonLabel);
    }

    /**
     * @param commitButtonLabel
     *            The text on the button to commit the entered value.
     * @since 1.4
     */
    public void setCommitButtonLabel(String commitButtonLabel) {
        this.commitButtonLabel = commitButtonLabel;
    }

    private String getLocalized(String text) {
        return (text != null) ? Messages.getLocalizedMessage(text) : ""; //$NON-NLS-1$
    }

}
