// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui;

import static org.openstreetmap.josm.tools.I18n.tr;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

import org.openstreetmap.josm.gui.help.HelpBrowser;
import org.openstreetmap.josm.gui.help.HelpUtil;
import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
import org.openstreetmap.josm.tools.GBC;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Utils;
import org.openstreetmap.josm.tools.WindowGeometry;

/**
 * General configurable dialog window.
 *
 * If dialog is modal, you can use {@link #getValue()} to retrieve the
 * button index. Note that the user can close the dialog
 * by other means. This is usually equivalent to cancel action.
 *
 * For non-modal dialogs, {@link #buttonAction(int, ActionEvent)} can be overridden.
 *
 * There are various options, see below.
 *
 * Note: The button indices are counted from 1 and upwards.
 * So for {@link #getValue()}, {@link #setDefaultButton(int)} and
 * {@link #setCancelButton} the first button has index 1.
 *
 * Simple example:
 * <pre>
 *  ExtendedDialog ed = new ExtendedDialog(
 *          Main.parent, tr("Dialog Title"),
 *          new String[] {tr("Ok"), tr("Cancel")});
 *  ed.setButtonIcons(new String[] {"ok", "cancel"});   // optional
 *  ed.setIcon(JOptionPane.WARNING_MESSAGE);            // optional
 *  ed.setContent(tr("Really proceed? Interesting things may happen..."));
 *  ed.showDialog();
 *  if (ed.getValue() == 1) { // user clicked first button "Ok"
 *      // proceed...
 *  }
 * </pre>
 */
public class ExtendedDialog extends JDialog {
    private final boolean disposeOnClose;
    private int result = 0;
    public static final int DialogClosedOtherwise = 0;
    private boolean toggleable = false;
    private String rememberSizePref = "";
    private WindowGeometry defaultWindowGeometry = null;
    private String togglePref = "";
    private int toggleValue = -1;
    private ConditionalOptionPaneUtil.MessagePanel togglePanel;
    private Component parent;
    private Component content;
    private final String[] bTexts;
    private String[] bToolTipTexts;
    private Icon[] bIcons;
    private List<Integer> cancelButtonIdx = Collections.emptyList();
    private int defaultButtonIdx = 1;
    protected JButton defaultButton = null;
    private Icon icon;
    private boolean modal;

    /** true, if the dialog should include a help button */
    private boolean showHelpButton;
    /** the help topic */
    private String helpTopic;

    /**
     * set to true if the content of the extended dialog should
     * be placed in a {@link JScrollPane}
     */
    private boolean placeContentInScrollPane;

    // For easy access when inherited
    protected Insets contentInsets = new Insets(10,5,0,5);
    protected List<JButton> buttons = new ArrayList<>();

    /**
     * This method sets up the most basic options for the dialog. Add more
     * advanced features with dedicated methods.
     * Possible features:
     * <ul>
     *   <li><code>setButtonIcons</code></li>
     *   <li><code>setContent</code></li>
     *   <li><code>toggleEnable</code></li>
     *   <li><code>toggleDisable</code></li>
     *   <li><code>setToggleCheckboxText</code></li>
     *   <li><code>setRememberWindowGeometry</code></li>
     * </ul>
     *
     * When done, call <code>showDialog</code> to display it. You can receive
     * the user's choice using <code>getValue</code>. Have a look at this function
     * for possible return values.
     *
     * @param parent       The parent element that will be used for position and maximum size
     * @param title        The text that will be shown in the window titlebar
     * @param buttonTexts  String Array of the text that will appear on the buttons. The first button is the default one.
     */
    public ExtendedDialog(Component parent, String title, String[] buttonTexts) {
        this(parent, title, buttonTexts, true, true);
    }

    /**
     * Same as above but lets you define if the dialog should be modal.
     * @param parent The parent element that will be used for position and maximum size
     * @param title The text that will be shown in the window titlebar
     * @param buttonTexts String Array of the text that will appear on the buttons. The first button is the default one.
     * @param modal Set it to {@code true} if you want the dialog to be modal
     */
    public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal) {
        this(parent, title, buttonTexts, modal, true);
    }

    public ExtendedDialog(Component parent, String title, String[] buttonTexts, boolean modal, boolean disposeOnClose) {
        super(JOptionPane.getFrameForComponent(parent), title, modal ? ModalityType.DOCUMENT_MODAL : ModalityType.MODELESS);
        this.parent = parent;
        this.modal = modal;
        bTexts = Utils.copyArray(buttonTexts);
        if (disposeOnClose) {
            setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        }
        this.disposeOnClose = disposeOnClose;
    }

    /**
     * Allows decorating the buttons with icons.
     * @param buttonIcons The button icons
     * @return {@code this}
     */
    public ExtendedDialog setButtonIcons(Icon[] buttonIcons) {
        this.bIcons = Utils.copyArray(buttonIcons);
        return this;
    }

    /**
     * Convenience method to provide image names instead of images.
     * @param buttonIcons The button icon names
     * @return {@code this}
     */
    public ExtendedDialog setButtonIcons(String[] buttonIcons) {
        bIcons = new Icon[buttonIcons.length];
        for (int i=0; i<buttonIcons.length; ++i) {
            bIcons[i] = ImageProvider.get(buttonIcons[i]);
        }
        return this;
    }

    /**
     * Allows decorating the buttons with tooltips. Expects a String array with
     * translated tooltip texts.
     *
     * @param toolTipTexts the tool tip texts. Ignored, if null.
     * @return {@code this}
     */
    public ExtendedDialog setToolTipTexts(String[] toolTipTexts) {
        this.bToolTipTexts = Utils.copyArray(toolTipTexts);
        return this;
    }

    /**
     * Sets the content that will be displayed in the message dialog.
     *
     * Note that depending on your other settings more UI elements may appear.
     * The content is played on top of the other elements though.
     *
     * @param content Any element that can be displayed in the message dialog
     * @return {@code this}
     */
    public ExtendedDialog setContent(Component content) {
        return setContent(content, true);
    }

    /**
     * Sets the content that will be displayed in the message dialog.
     *
     * Note that depending on your other settings more UI elements may appear.
     * The content is played on top of the other elements though.
     *
     * @param content Any element that can be displayed in the message dialog
     * @param placeContentInScrollPane if  true, places  the content in a JScrollPane
     * @return {@code this}
     */
    public ExtendedDialog setContent(Component content, boolean placeContentInScrollPane) {
        this.content = content;
        this.placeContentInScrollPane = placeContentInScrollPane;
        return this;
    }

    /**
     * Sets the message that will be displayed. The String will be automatically
     * wrapped if it is too long.
     *
     * Note that depending on your other settings more UI elements may appear.
     * The content is played on top of the other elements though.
     *
     * @param message The text that should be shown to the user
     * @return {@code this}
     */
    public ExtendedDialog setContent(String message) {
        return setContent(string2label(message), false);
    }

    /**
     * Decorate the dialog with an icon that is shown on the left part of
     * the window area. (Similar to how it is done in {@link JOptionPane})
     * @param icon The icon to display
     * @return {@code this}
     */
    public ExtendedDialog setIcon(Icon icon) {
        this.icon = icon;
        return this;
    }

    /**
     * Convenience method to allow values that would be accepted by {@link JOptionPane} as messageType.
     * @param messageType The {@link JOptionPane} messageType
     * @return {@code this}
     */
    public ExtendedDialog setIcon(int messageType) {
        switch (messageType) {
            case JOptionPane.ERROR_MESSAGE:
                return setIcon(UIManager.getIcon("OptionPane.errorIcon"));
            case JOptionPane.INFORMATION_MESSAGE:
                return setIcon(UIManager.getIcon("OptionPane.informationIcon"));
            case JOptionPane.WARNING_MESSAGE:
                return setIcon(UIManager.getIcon("OptionPane.warningIcon"));
            case JOptionPane.QUESTION_MESSAGE:
                return setIcon(UIManager.getIcon("OptionPane.questionIcon"));
            case JOptionPane.PLAIN_MESSAGE:
                return setIcon(null);
            default:
                throw new IllegalArgumentException("Unknown message type!");
        }
    }

    /**
     * Show the dialog to the user. Call this after you have set all options
     * for the dialog. You can retrieve the result using {@link #getValue()}.
     * @return {@code this}
     */
    public ExtendedDialog showDialog() {
        // Check if the user has set the dialog to not be shown again
        if (toggleCheckState()) {
            result = toggleValue;
            return this;
        }

        setupDialog();
        if (defaultButton != null) {
            getRootPane().setDefaultButton(defaultButton);
        }
        fixFocus();
        setVisible(true);
        toggleSaveState();
        return this;
    }

    /**
     * Retrieve the user choice after the dialog has been closed.
     *
     * @return <ul> <li>The selected button. The count starts with 1.</li>
     *              <li>A return value of {@link #DialogClosedOtherwise} means the dialog has been closed otherwise.</li>
     *         </ul>
     */
    public int getValue() {
        return result;
    }

    private boolean setupDone = false;

    /**
     * This is called by {@link #showDialog()}.
     * Only invoke from outside if you need to modify the contentPane
     */
    public void setupDialog() {
        if (setupDone)
            return;
        setupDone = true;

        setupEscListener();

        JButton button;
        JPanel buttonsPanel = new JPanel(new GridBagLayout());

        for (int i=0; i < bTexts.length; i++) {
            final int final_i = i;
            Action action = new AbstractAction(bTexts[i]) {
                @Override public void actionPerformed(ActionEvent evt) {
                    buttonAction(final_i, evt);
                }
            };

            button = new JButton(action);
            if (i == defaultButtonIdx-1) {
                defaultButton = button;
            }
            if(bIcons != null && bIcons[i] != null) {
                button.setIcon(bIcons[i]);
            }
            if (bToolTipTexts != null && i < bToolTipTexts.length && bToolTipTexts[i] != null) {
                button.setToolTipText(bToolTipTexts[i]);
            }

            buttonsPanel.add(button, GBC.std().insets(2,2,2,2));
            buttons.add(button);
        }
        if (showHelpButton) {
            buttonsPanel.add(new JButton(new HelpAction()), GBC.std().insets(2,2,2,2));
            HelpUtil.setHelpContext(getRootPane(),helpTopic);
        }

        JPanel cp = new JPanel(new GridBagLayout());

        GridBagConstraints gc = new GridBagConstraints();
        gc.gridx = 0;
        int y = 0;
        gc.gridy = y++;
        gc.weightx = 0.0;
        gc.weighty = 0.0;

        if (icon != null) {
            JLabel iconLbl = new JLabel(icon);
            gc.insets = new Insets(10,10,10,10);
            gc.anchor = GridBagConstraints.NORTH;
            gc.weighty = 1.0;
            cp.add(iconLbl, gc);
            gc.anchor = GridBagConstraints.CENTER;
            gc.gridx = 1;
        }

        gc.fill = GridBagConstraints.BOTH;
        gc.insets = contentInsets;
        gc.weightx = 1.0;
        gc.weighty = 1.0;
        cp.add(content, gc);

        gc.fill = GridBagConstraints.NONE;
        gc.gridwidth = GridBagConstraints.REMAINDER;
        gc.weightx = 0.0;
        gc.weighty = 0.0;

        if (toggleable) {
            togglePanel = new ConditionalOptionPaneUtil.MessagePanel(null, ConditionalOptionPaneUtil.isInBulkOperation(togglePref));
            gc.gridx = icon != null ? 1 : 0;
            gc.gridy = y++;
            gc.anchor = GridBagConstraints.LINE_START;
            gc.insets = new Insets(5,contentInsets.left,5,contentInsets.right);
            cp.add(togglePanel, gc);
        }

        gc.gridy = y++;
        gc.anchor = GridBagConstraints.CENTER;
            gc.insets = new Insets(5,5,5,5);
        cp.add(buttonsPanel, gc);
        if (placeContentInScrollPane) {
            JScrollPane pane = new JScrollPane(cp);
            pane.setBorder(null);
            setContentPane(pane);
        } else {
            setContentPane(cp);
        }
        pack();

        // Try to make it not larger than the parent window or at least not larger than 2/3 of the screen
        Dimension d = getSize();
        Dimension x = findMaxDialogSize();

        boolean limitedInWidth = d.width > x.width;
        boolean limitedInHeight = d.height > x.height;

        if(x.width  > 0 && d.width  > x.width) {
            d.width  = x.width;
        }
        if(x.height > 0 && d.height > x.height) {
            d.height = x.height;
        }

        // We have a vertical scrollbar and enough space to prevent a horizontal one
        if(!limitedInWidth && limitedInHeight) {
            d.width += new JScrollBar().getPreferredSize().width;
        }

        setSize(d);
        setLocationRelativeTo(parent);
    }

    /**
     * This gets performed whenever a button is clicked or activated
     * @param buttonIndex the button index (first index is 0)
     * @param evt the button event
     */
    protected void buttonAction(int buttonIndex, ActionEvent evt) {
        result = buttonIndex+1;
        setVisible(false);
    }

    /**
     * Tries to find a good value of how large the dialog should be
     * @return Dimension Size of the parent Component or 2/3 of screen size if not available
     */
    protected Dimension findMaxDialogSize() {
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension x = new Dimension(screenSize.width*2/3, screenSize.height*2/3);
        if (parent != null) {
            x = JOptionPane.getFrameForComponent(parent).getSize();
        }
        return x;
    }

    /**
     * Makes the dialog listen to ESC keypressed
     */
    private void setupEscListener() {
        Action actionListener = new AbstractAction() {
            @Override public void actionPerformed(ActionEvent actionEvent) {
                // 0 means that the dialog has been closed otherwise.
                // We need to set it to zero again, in case the dialog has been re-used
                // and the result differs from its default value
                result = ExtendedDialog.DialogClosedOtherwise;
                setVisible(false);
            }
        };

        getRootPane().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
            .put(KeyStroke.getKeyStroke("ESCAPE"), "ESCAPE");
        getRootPane().getActionMap().put("ESCAPE", actionListener);
    }

    protected final void rememberWindowGeometry(WindowGeometry geometry) {
        if (geometry != null) {
            geometry.remember(rememberSizePref);
        }
    }

    protected final WindowGeometry initWindowGeometry() {
        return new WindowGeometry(rememberSizePref, defaultWindowGeometry);
    }

    /**
     * Override setVisible to be able to save the window geometry if required
     */
    @Override
    public void setVisible(boolean visible) {
        if (visible) {
            repaint();
        }

        // Ensure all required variables are available
        if(rememberSizePref.length() != 0 && defaultWindowGeometry != null) {
            if(visible) {
                initWindowGeometry().applySafe(this);
            } else if (isShowing()) { // should fix #6438, #6981, #8295
                rememberWindowGeometry(new WindowGeometry(this));
            }
        }
        super.setVisible(visible);

        if (!visible && disposeOnClose) {
            dispose();
        }
    }

    /**
     * Call this if you want the dialog to remember the geometry (size and position) set by the user.
     * Set the pref to <code>null</code> or to an empty string to disable again.
     * By default, it's disabled.
     *
     * Note: If you want to set the width of this dialog directly use the usual
     * setSize, setPreferredSize, setMaxSize, setMinSize
     *
     * @param pref  The preference to save the dimension to
     * @param wg    The default window geometry that should be used if no
     *              existing preference is found (only takes effect if
     *              <code>pref</code> is not null or empty
     * @return {@code this}
     */
    public ExtendedDialog setRememberWindowGeometry(String pref, WindowGeometry wg) {
        rememberSizePref = pref == null ? "" : pref;
        defaultWindowGeometry = wg;
        return this;
    }

    /**
     * Calling this will offer the user a "Do not show again" checkbox for the
     * dialog. Default is to not offer the choice; the dialog will be shown
     * every time.
     * Currently, this is not supported for non-modal dialogs.
     * @param togglePref  The preference to save the checkbox state to
     * @return {@code this}
     */
    public ExtendedDialog toggleEnable(String togglePref) {
        if (!modal) {
            throw new IllegalArgumentException();
        }
        this.toggleable = true;
        this.togglePref = togglePref;
        return this;
    }

    /**
     * Call this if you "accidentally" called toggleEnable. This doesn't need
     * to be called for every dialog, as it's the default anyway.
     * @return {@code this}
     */
    public ExtendedDialog toggleDisable() {
        this.toggleable = false;
        return this;
    }

    /**
     * Sets the button that will react to ENTER.
     * @param defaultButtonIdx The button index (starts to )
     * @return {@code this}
     */
    public ExtendedDialog setDefaultButton(int defaultButtonIdx) {
        this.defaultButtonIdx = defaultButtonIdx;
        return this;
    }

    /**
     * Used in combination with toggle:
     * If the user presses 'cancel' the toggle settings are ignored and not saved to the pref
     * @param cancelButtonIdx index of the button that stands for cancel, accepts multiple values
     * @return {@code this}
     */
    public ExtendedDialog setCancelButton(Integer... cancelButtonIdx) {
        this.cancelButtonIdx = Arrays.<Integer>asList(cancelButtonIdx);
        return this;
    }

    /**
     * Don't focus the "do not show this again" check box, but the default button.
     */
    protected void fixFocus() {
        if (toggleable && defaultButton != null) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override public void run() {
                    defaultButton.requestFocusInWindow();
                }
            });
        }
    }

    /**
     * This function returns true if the dialog has been set to "do not show again"
     * @return true if dialog should not be shown again
     */
    public final boolean toggleCheckState() {
        toggleable = togglePref != null && !togglePref.isEmpty();
        toggleValue = ConditionalOptionPaneUtil.getDialogReturnValue(togglePref);
        return toggleable && toggleValue != -1;
    }

    /**
     * This function checks the state of the "Do not show again" checkbox and
     * writes the corresponding pref.
     */
    private void toggleSaveState() {
        if (!toggleable ||
                togglePanel == null ||
                cancelButtonIdx.contains(result) ||
                result == ExtendedDialog.DialogClosedOtherwise)
            return;
        togglePanel.getNotShowAgain().store(togglePref, result);
    }

    /**
     * Convenience function that converts a given string into a JMultilineLabel
     * @param msg
     * @return JMultilineLabel
     */
    private static JMultilineLabel string2label(String msg) {
        JMultilineLabel lbl = new JMultilineLabel(msg);
        // Make it not wider than 1/2 of the screen
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        lbl.setMaxWidth(screenSize.width/2);
        return lbl;
    }

    /**
     * Configures how this dialog support for context sensitive help.
     * <ul>
     *  <li>if helpTopic is null, the dialog doesn't provide context sensitive help</li>
     *  <li>if helpTopic != null, the dialog redirect user to the help page for this helpTopic when
     *  the user clicks F1 in the dialog</li>
     *  <li>if showHelpButton is true, the dialog displays "Help" button (rightmost button in
     *  the button row)</li>
     * </ul>
     *
     * @param helpTopic the help topic
     * @param showHelpButton true, if the dialog displays a help button
     * @return {@code this}
     */
    public ExtendedDialog configureContextsensitiveHelp(String helpTopic, boolean showHelpButton) {
        this.helpTopic = helpTopic;
        this.showHelpButton = showHelpButton;
        return this;
    }

    class HelpAction extends AbstractAction {
        public HelpAction() {
            putValue(SHORT_DESCRIPTION, tr("Show help information"));
            putValue(NAME, tr("Help"));
            putValue(SMALL_ICON, ImageProvider.get("help"));
        }

        @Override public void actionPerformed(ActionEvent e) {
            HelpBrowser.setUrlForHelpTopic(helpTopic);
        }
    }
}
