/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.openide;

import org.openide.util.Utilities;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.swing.*;
import org.openide.util.ImageUtilities;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;


/** Permits dialogs to be displayed.
 * @author Jesse Glick
 * @since 3.14
 */
public abstract class DialogDisplayer {
    /** Subclass constructor. */
    protected DialogDisplayer() {
    }

    /** Get the default dialog displayer.
     * @return the default instance from lookup
     */
    public static DialogDisplayer getDefault() {
        DialogDisplayer dd = Lookup.getDefault ().lookup (DialogDisplayer.class);

        if (dd == null) {
            dd = new Trivial();
        }

        return dd;
    }

    /** Notify the user of something in a message box, possibly with feedback.
     * <p>To support both GUI and non-GUI use, this method may be called
     * from any thread (providing you are not holding any locks), and
     * will block the caller's thread. In GUI mode, it will be run in the AWT
     * event thread automatically. If you wish to hold locks, or do not
     * need the result object immediately or at all, please make this call
     * asynchronously (e.g. from the request processor).
     * @param descriptor description of the notification
     * @return the option that caused the message box to be closed
     */
    public abstract Object notify(NotifyDescriptor descriptor);

    /** Notify the user of something in a message box, possibly with feedback,
     * this method may be called
     * from any thread. The thread will return immediately and
     * the dialog will be shown <q>later</q>, usually when AWT thread
     * is empty and can handle the request.
     * 
     * <p class="non-normative">
     * Implementation note: Since version 7.3, implementation improved to work
     * also before main window is opened. For example: When method is called
     * from ModuleInstall.restored, then modal dialog is opened and blocks main
     * window until dialog is closed. Typical use case is login dialog.
     * </p>
     *
     * @param descriptor description of the notification
     * @since 7.0
     */
    public void notifyLater(final NotifyDescriptor descriptor) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                DialogDisplayer.this.notify(descriptor);
            }
        });
    }

    /** Get a new standard dialog.
     * The dialog is designed and created as specified in the parameter.
     * Anyone who wants a dialog with standard buttons and
     * standard behavior should use this method.
     * <p><strong>Do not cache</strong> the resulting dialog if it
     * is modal and try to reuse it! Always create a new dialog
     * using this method if you need to show a dialog again.
     * Otherwise previously closed windows can reappear.
     * @param descriptor general description of the dialog
     * @return the new dialog
     */
    public abstract Dialog createDialog(DialogDescriptor descriptor);
    
    /**
     * Same as #createDialog(org.openide.DialogDescriptor) except that it's possible
     * to specify dialog's parent Frame window. When a document window is floated
     * and has focus then new dialog window will use it as a parent window by default.
     * That means non-modal dialogs will close when that document window is closed.
     * To avoid such situation pass WindowManager.getDefault().getMainWindow() as
     * dialog parent window.
     * @param descriptor general description of the dialog
     * @param parent Dialgo parent frame.
     * @return New dialog
     * @since 7.38
     */
    public Dialog createDialog(DialogDescriptor descriptor, Frame parent) {
        return createDialog(descriptor);
    }

    /**
     * Minimal implementation suited for standalone usage.
     * @see "#30031"
     */
    private static final class Trivial extends DialogDisplayer {
        @Override
        public Object notify(NotifyDescriptor nd) {
            if (GraphicsEnvironment.isHeadless()) {
                return NotifyDescriptor.CLOSED_OPTION;
            }

            JDialog dialog = new StandardDialog(nd.getTitle(), true, nd, null, null);
            dialog.setVisible(true);

            return (nd.getValue() != null) ? nd.getValue() : NotifyDescriptor.CLOSED_OPTION;
        }

        @Override
        public Dialog createDialog(final DialogDescriptor dd) {
            final StandardDialog dialog = new StandardDialog(
                    dd.getTitle(), dd.isModal(), dd, dd.getClosingOptions(), dd.getButtonListener()
                );
            dd.addPropertyChangeListener(new DialogUpdater(dialog, dd));

            return dialog;
        }

        /**
         * Given a message object, create a displayable component from it.
         */
        private static Component message2Component(Object message) {
            if (message instanceof Component) {
                return (Component) message;
            } else if (message instanceof Object[]) {
                Object[] sub = (Object[]) message;
                JPanel panel = new JPanel();
                panel.setLayout(new FlowLayout());

                for (int i = 0; i < sub.length; i++) {
                    panel.add(message2Component(sub[i]));
                }

                return panel;
            } else if (message instanceof Icon) {
                return new JLabel((Icon) message);
            } else {
                // bugfix #35742, used JTextArea to correctly word-wrapping
                String text = message.toString();
                JTextArea area = new JTextArea(text);
                Color c = UIManager.getColor("Label.background"); // NOI18N

                if (c != null) {
                    area.setBackground(c);
                }

                area.setLineWrap(true);
                area.setWrapStyleWord(true);
                area.setEditable(false);
                area.setTabSize(4); // looks better for module sys messages than 8

                area.setColumns(40);

                if (text.indexOf('\n') != -1) {
                    // Complex multiline message.
                    return new JScrollPane(area);
                } else {
                    // Simple message.
                    return area;
                }
            }
        }

        private static Component option2Button(Object option, NotifyDescriptor nd, ActionListener l, JRootPane rp) {
            if (option instanceof AbstractButton) {
                AbstractButton b = (AbstractButton) option;
                removeOldListeners(b);
                b.addActionListener(l);

                return b;
            } else if (option instanceof Component) {
                return (Component) option;
            } else if (option instanceof Icon) {
                return new JLabel((Icon) option);
            } else {
                String text;
                boolean defcap;

                if (option == NotifyDescriptor.OK_OPTION) {
                    text = NbBundle.getMessage(DialogDisplayer.class, "CTL_OK");
                    defcap = true;
                } else if (option == NotifyDescriptor.CANCEL_OPTION) {
                    text = NbBundle.getMessage(DialogDisplayer.class, "CTL_CANCEL");
                    defcap = false;
                } else if (option == NotifyDescriptor.YES_OPTION) {
                    text = NbBundle.getMessage(DialogDisplayer.class, "CTL_YES");
                    defcap = true;
                } else if (option == NotifyDescriptor.NO_OPTION) {
                    text = NbBundle.getMessage(DialogDisplayer.class, "CTL_NO");
                    defcap = false;
                } else if (option == NotifyDescriptor.CLOSED_OPTION) {
                    throw new IllegalArgumentException();
                } else {
                    text = option.toString();
                    defcap = false;
                }

                JButton b = new JButton(text);

                if (defcap && (rp.getDefaultButton() == null)) {
                    rp.setDefaultButton(b);
                }

                // added a simple accessible name to buttons
                b.getAccessibleContext().setAccessibleName(text);
                b.addActionListener(l);

                return b;
            }
        }

        private static void removeOldListeners( AbstractButton button ) {
            ArrayList<ActionListener> toRem = new ArrayList<ActionListener>();
            for( ActionListener al : button.getActionListeners() ) {
                if( al instanceof StandardDialog.ButtonListener ) {
                    toRem.add( al );
                }
            }
            for( ActionListener al : toRem ) {
                button.removeActionListener( al );
            }
                            }

        private static final class StandardDialog extends JDialog {
            final NotifyDescriptor nd;
            private Component messageComponent;
            private final JPanel buttonPanel;
            private final Object[] closingOptions;
            private final ActionListener buttonListener;
            private boolean haveFinalValue = false;
            private Color nbErrorForeground;
            private Color nbWarningForeground;
            private Color nbInfoForeground;
            private JLabel notificationLine;
            private static final int MSG_TYPE_ERROR = 1;
            private static final int MSG_TYPE_WARNING = 2;
            private static final int MSG_TYPE_INFO = 3;


            public StandardDialog(
                String title, boolean modal, NotifyDescriptor nd, Object[] closingOptions, ActionListener buttonListener
            ) {
                super((Frame) null, title, modal);
                this.nd = nd;
                this.closingOptions = closingOptions;
                this.buttonListener = buttonListener;
                getContentPane().setLayout(new BorderLayout());
                setDefaultCloseOperation(nd.isNoDefaultClose()
                        ? WindowConstants.DO_NOTHING_ON_CLOSE
                        : WindowConstants.DISPOSE_ON_CLOSE);
                updateMessage();
                buttonPanel = new JPanel();
                buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
                updateOptions();
                getContentPane().add(buttonPanel, BorderLayout.SOUTH, 1);

                KeyStroke k = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
                Object actionKey = "cancel"; // NOI18N
                getRootPane().getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(k, actionKey);

                Action cancelAction = new AbstractAction() {
                        @Override
                        public void actionPerformed(ActionEvent ev) {
                            if( !StandardDialog.this.nd.isNoDefaultClose() )
                                cancel();
                        }
                    };

                getRootPane().getActionMap().put(actionKey, cancelAction);
                addWindowListener(
                    new WindowAdapter() {
                        public void windowClosing(WindowEvent ev) {
                            if (!haveFinalValue) {
                                StandardDialog.this.nd.setValue(NotifyDescriptor.CLOSED_OPTION);
                            }
                        }
                    }
                );
                pack();

                Rectangle r = Utilities.getUsableScreenBounds();
                int maxW = (r.width * 9) / 10;
                int maxH = (r.height * 9) / 10;
                Dimension d = getPreferredSize();
                d.width = Math.min(d.width, maxW);
                d.height = Math.min(d.height, maxH);
                setBounds(Utilities.findCenterBounds(d));
            }

            private void cancel() {
                nd.setValue(NotifyDescriptor.CANCEL_OPTION);
                haveFinalValue = true;
                dispose();
            }

            public void updateMessage() {
                if (messageComponent != null) {
                    getContentPane().remove(messageComponent);
                }

                //System.err.println("updateMessage: " + nd.getMessage());
                messageComponent = message2Component(nd.getMessage());
                if (! (nd instanceof WizardDescriptor) && nd.getNotificationLineSupport () != null) {
                    JComponent toAdd = new JPanel (new BorderLayout ());
                    toAdd.add (messageComponent, BorderLayout.CENTER);

                    nbErrorForeground = UIManager.getColor("nb.errorForeground"); //NOI18N
                    if (nbErrorForeground == null) {
                        //nbErrorForeground = new Color(89, 79, 191); // RGB suggested by Bruce in #28466
                        nbErrorForeground = new Color(255, 0, 0); // RGB suggested by jdinga in #65358
                    }

                    nbWarningForeground = UIManager.getColor("nb.warningForeground"); //NOI18N
                    if (nbWarningForeground == null) {
                        nbWarningForeground = new Color(51, 51, 51); // Label.foreground
                    }

                    nbInfoForeground = UIManager.getColor("nb.warningForeground"); //NOI18N
                    if (nbInfoForeground == null) {
                        nbInfoForeground = UIManager.getColor("Label.foreground"); //NOI18N
                    }

                    notificationLine = new FixedHeightLabel ();
                    NotificationLineSupport nls = nd.getNotificationLineSupport ();
                    if (nls.getInformationMessage () != null) {
                        updateNotificationLine (this, MSG_TYPE_INFO, nls.getInformationMessage ());
                    } else if (nls.getWarningMessage () != null) {
                        updateNotificationLine (this, MSG_TYPE_WARNING, nls.getWarningMessage ());
                    } else if (nls.getErrorMessage () != null) {
                        updateNotificationLine (this, MSG_TYPE_ERROR, nls.getErrorMessage ());
                    }
                    toAdd.add (notificationLine, BorderLayout.SOUTH);
                    messageComponent = toAdd;
                }
                getContentPane().add(messageComponent, BorderLayout.CENTER);
            }

            public void updateOptions() {
                Set<Object> addedOptions = new HashSet<Object>(5);
                Object[] options = nd.getOptions();

                if (options == null) {
                    switch (nd.getOptionType()) {
                    case NotifyDescriptor.DEFAULT_OPTION:
                    case NotifyDescriptor.OK_CANCEL_OPTION:
                        if (!Utilities.isMac()) {
                            // Windows UI Guidelines
                            options = new Object[] { NotifyDescriptor.OK_OPTION, NotifyDescriptor.CANCEL_OPTION, };
                        } else {
                            // see http://netbeans.org/bugzilla/show_bug.cgi?id=202784 - according to
                            // Apple HIG guidelines 'Cancel' should be on the left
                            // http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html#//apple_ref/doc/uid/20000961-TP9
                            options = new Object[] { NotifyDescriptor.CANCEL_OPTION, NotifyDescriptor.OK_OPTION, };
                        }
                        break;

                    case NotifyDescriptor.YES_NO_OPTION:
                        if (!Utilities.isMac()) {
                            // Windows UI Guidelines
                            options = new Object[] { NotifyDescriptor.YES_OPTION, NotifyDescriptor.NO_OPTION, };
                        } else {
                            // see http://netbeans.org/bugzilla/show_bug.cgi?id=202784 - according to
                            // Apple HIG guidelines 'Cancel' should be on the left
                            // http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html#//apple_ref/doc/uid/20000961-TP9
                            options = new Object[] { NotifyDescriptor.NO_OPTION, NotifyDescriptor.YES_OPTION, };
                        }

                        break;

                    case NotifyDescriptor.YES_NO_CANCEL_OPTION:
                        if (!Utilities.isMac()) {
                            // Windows UI Guidelines
                            options = new Object[] {
                                NotifyDescriptor.YES_OPTION, NotifyDescriptor.NO_OPTION, NotifyDescriptor.CANCEL_OPTION,
                            };
                        } else {
                            // see http://netbeans.org/bugzilla/show_bug.cgi?id=202784 - according to
                            // Apple HIG guidelines 'Cancel' should be on the left
                            // http://developer.apple.com/library/mac/#documentation/UserExperience/Conceptual/AppleHIGuidelines/Windows/Windows.html#//apple_ref/doc/uid/20000961-TP9
                            options = new Object[] {
                                NotifyDescriptor.NO_OPTION, NotifyDescriptor.CANCEL_OPTION, NotifyDescriptor.YES_OPTION,
                            };
                        }

                        break;

                    default:
                        throw new IllegalArgumentException();
                    }
                }

                //System.err.println("prep: " + Arrays.asList(options) + " " + Arrays.asList(closingOptions) + " " + buttonListener);
                buttonPanel.removeAll();

                JRootPane rp = getRootPane();

                for (int i = 0; i < options.length; i++) {
                    addedOptions.add(options[i]);
                    buttonPanel.add(option2Button(options[i], nd, makeListener(options[i]), rp));
                }

                options = nd.getAdditionalOptions();

                if (options != null) {
                    for (int i = 0; i < options.length; i++) {
                        addedOptions.add(options[i]);
                        buttonPanel.add(option2Button(options[i], nd, makeListener(options[i]), rp));
                    }
                }

                if (closingOptions != null) {
                    for (int i = 0; i < closingOptions.length; i++) {
                        if (addedOptions.add(closingOptions[i])) {
                            ActionListener l = makeListener(closingOptions[i]);
                            attachActionListener(closingOptions[i], l);
                        }
                    }
                }
            }

            private void attachActionListener(Object comp, ActionListener l) {
                // on JButtons attach simply by method call
                if (comp instanceof JButton) {
                    JButton b = (JButton) comp;
                    b.addActionListener(l);

                    return;
                } else {
                    // we will have to use dynamic method invocation to add the action listener
                    // to generic component (and we succeed only if it has the addActionListener method)
                    java.lang.reflect.Method m;

                    try {
                        m = comp.getClass().getMethod("addActionListener", new Class[] { ActionListener.class }); // NOI18N

                        try {
                            m.setAccessible(true);
                        } catch (SecurityException se) {
                            m = null; // no jo, we cannot make accessible
                        }
                    } catch (NoSuchMethodException e) {
                        m = null; // no jo, we cannot attach ActionListener to this Component
                    } catch (SecurityException e2) {
                        m = null; // no jo, we cannot attach ActionListener to this Component
                    }

                    if (m != null) {
                        try {
                            m.invoke(comp, new Object[] { l });
                        } catch (Exception e) {
                            // not succeeded, so give up
                        }
                    }
                }
            }

            private ActionListener makeListener(final Object option) {
                return new ButtonListener( option );
            }
            
            private class ButtonListener implements ActionListener {

                private final Object option;
                
                public ButtonListener( Object option ) {
                    this.option = option;
                }
                
                @Override
                public void actionPerformed( ActionEvent e ) {
                    nd.setValue(option);

                    if (buttonListener != null) {
                        // #34485: some listeners expect that the action source is the option, not the button
                        ActionEvent e2 = new ActionEvent(
                                option, e.getID(), e.getActionCommand(), e.getWhen(), e.getModifiers()
                            );
                        buttonListener.actionPerformed(e2);
                    }

                    if ((closingOptions == null) || Arrays.asList(closingOptions).contains(option)) {
                        haveFinalValue = true;
                        setVisible(false);
                    }
                }
            }     
        }

        private static class DialogUpdater implements PropertyChangeListener {

            private StandardDialog dialog;

            private DialogDescriptor dd;

            public DialogUpdater(StandardDialog dialog, DialogDescriptor dd) {
                super();
                this.dialog = dialog;
                this.dd = dd;
            }

            public void propertyChange(final PropertyChangeEvent ev) {
                if( !SwingUtilities.isEventDispatchThread() ) {
                    SwingUtilities.invokeLater(new Runnable() {
                        public void run() {
                            propertyChange(ev);
                        }
                    });
                    return;
                }
                String pname = ev.getPropertyName();
                if (NotifyDescriptor.PROP_TITLE.equals(pname)) {
                    dialog.setTitle(dd.getTitle());
                } else if (NotifyDescriptor.PROP_NO_DEFAULT_CLOSE.equals(pname)) {
                    dialog.setDefaultCloseOperation(dd.isNoDefaultClose() ? JDialog.DO_NOTHING_ON_CLOSE : JDialog.DISPOSE_ON_CLOSE);
                } else
                    if (NotifyDescriptor.PROP_MESSAGE.equals(pname)) {
                        dialog.updateMessage();
                        dialog.validate();
                        dialog.repaint();
                    } else
                        if (NotifyDescriptor.PROP_OPTIONS.equals(pname) || NotifyDescriptor.PROP_OPTION_TYPE.equals(pname)) {
                            dialog.updateOptions();
                            dialog.validate();
                            dialog.repaint();
                    } else if (NotifyDescriptor.PROP_INFO_NOTIFICATION.equals (ev.getPropertyName ())) {
                        updateNotificationLine (dialog, StandardDialog.MSG_TYPE_INFO, ev.getNewValue ());
                    } else if (NotifyDescriptor.PROP_WARNING_NOTIFICATION.equals (ev.getPropertyName ())) {
                        updateNotificationLine (dialog, StandardDialog.MSG_TYPE_WARNING, ev.getNewValue ());
                    } else if (NotifyDescriptor.PROP_ERROR_NOTIFICATION.equals (ev.getPropertyName ())) {
                        updateNotificationLine (dialog, StandardDialog.MSG_TYPE_ERROR, ev.getNewValue ());
                    }
            }
        }

        private static void updateNotificationLine (StandardDialog dialog, int msgType, Object o) {
            String msg = o == null ? null : o.toString ();
            if (msg != null && msg.trim().length() > 0) {
                switch (msgType) {
                    case StandardDialog.MSG_TYPE_ERROR:
                        prepareMessage(dialog.notificationLine, ImageUtilities.loadImageIcon("org/netbeans/modules/dialogs/error.gif", false), //NOI18N
                            dialog.nbErrorForeground);
                        break;
                    case StandardDialog.MSG_TYPE_WARNING:
                        prepareMessage(dialog.notificationLine, ImageUtilities.loadImageIcon("org/netbeans/modules/dialogs/warning.gif", false), //NOI18N
                            dialog.nbWarningForeground);
                        break;
                    case StandardDialog.MSG_TYPE_INFO:
                        prepareMessage(dialog.notificationLine, ImageUtilities.loadImageIcon("org/netbeans/modules/dialogs/info.png", false), //NOI18N
                            dialog.nbInfoForeground);
                        break;
                    default:
                }
                dialog.notificationLine.setToolTipText (msg);
            } else {
                prepareMessage(dialog.notificationLine, null, null);
                dialog.notificationLine.setToolTipText (null);
            }
            dialog.notificationLine.setText(msg);
        }

        private static void prepareMessage(JLabel label, ImageIcon icon, Color fgColor) {
            label.setIcon(icon);
            label.setForeground(fgColor);
        }

        private static final class FixedHeightLabel extends JLabel {

            private static final int ESTIMATED_HEIGHT = 16;

            public FixedHeightLabel () {
                super ();
            }

            @Override
            public Dimension getPreferredSize() {
                Dimension preferredSize = super.getPreferredSize();
                assert ESTIMATED_HEIGHT == ImageUtilities.loadImage ("org/netbeans/modules/dialogs/warning.gif").getHeight (null) : "Use only 16px icon."; //NOI18N
                preferredSize.height = Math.max (ESTIMATED_HEIGHT, preferredSize.height);
                return preferredSize;
            }
        }

    }
}
