/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2001, Institut de Recherche pour le Développement
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation; either
 *    version 2.1 of the License, or (at your option) any later version.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotools.resources;

// J2SE library
import java.util.Locale;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Frame;
import java.awt.event.ActionListener;
import java.awt.event.WindowListener;
import java.awt.IllegalComponentStateException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;

// Swing dependencies
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDesktopPane;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JInternalFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextArea;
import javax.swing.LookAndFeel;
import javax.swing.event.InternalFrameListener;

// Geotools dependencies
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;


/**
 * A collection of utility methods for Swing.  All <code>show*</code> methods delegate
 * their work to the corresponding method in {@link JOptionPane}, with two differences:
 *
 * <ul>
 *   <li>{@code SwingUtilities}'s method may be invoked from any thread. If they
 *       are invoked from a non-Swing thread, execution will be delegate to the Swing
 *       thread and the calling thread will block until completion.</li>
 *   <li>If a parent component is a {@link JDesktopPane}, dialogs will be rendered as
 *       internal frames instead of frames.</li>
 * </ul>
 *
 * @since 2.0
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/resources/SwingUtilities.java $
 * @version $Id: SwingUtilities.java 22443 2006-10-27 20:47:22Z desruisseaux $
 * @author Martin Desruisseaux
 */
public final class SwingUtilities {
    /**
     * Do not allow any instance
     * of this class to be created.
     */
    private SwingUtilities() {
    }

    /**
     * Insert a Swing component into a frame. The kind of frame depends on the owner:
     *
     * <ul>
     *   <li>If {@code owner} or one of its parent is a {@link JDesktopPane},
     *       then {@code panel} is added into a {@link JInternalFrame}.</li>
     *   <li>If {@code owner} or one of its parent is a {@link Frame} or a {@link Dialog},
     *       then {@code panel} is added into a {@link JDialog}.</li>
     *   <li>Otherwise, {@code panel} is added into a {@link JFrame}.</li>
     * </ul>
     *
     * @param  owner The frame's owner, or {@code null} if none.
     * @param  panel The panel to insert into a frame.
     * @param  title The frame's title.
     * @param  listener A listener to receives frame events.  If non-null, then this listener will
     *         be registered to whatever kind of frame this method will constructs. In the special
     *         case where this method constructs an {@linkplain JInternalFrame internal frame} and
     *         the {@code listener} is not an instance of {@link InternalFrameListener}, then
     *         this method will wrap the {@code listener} into an {@code InternalFrameListener}.
     * @return The frame. This frame is not initially visible. The method
     *         {@code Component.setVisible(true)} must be invoked in order to show the frame.
     */
    public static Component toFrame(Component owner,
                                    final JComponent     panel,
                                    final String         title,
                                    final WindowListener listener)
    {
        while (owner != null) {
            if (owner == panel) {
                throw new IllegalArgumentException();
            }
            // NOTE: All 'addFooListener(...)' below ignore null argument. No need to check ourself.
            if (owner instanceof JDesktopPane) {
                final JInternalFrame frame = new JInternalFrame(title, true, true, true, true);
                frame.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
                frame.addInternalFrameListener(InternalWindowListener.wrap(listener));
                ((JDesktopPane) owner).add(frame);
                frame.getContentPane().add(panel);
                frame.pack();
                return frame;
            }
            if (owner instanceof Frame) {
                final JDialog dialog = new JDialog((Frame) owner, title);
                dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
                dialog.addWindowListener(listener);
                dialog.getContentPane().add(panel);
                dialog.pack();
                return dialog;
            }
            if (owner instanceof Dialog) {
                final JDialog dialog = new JDialog((Dialog) owner, title);
                dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
                dialog.addWindowListener(listener);
                dialog.getContentPane().add(panel);
                dialog.pack();
                return dialog;
            }
            owner = owner.getParent();
        }
        //
        // Add the panel as a standalone window.
        // This window has its own button on the task bar.
        //
        final JFrame frame = new JFrame(title);
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.addWindowListener(listener);
        frame.getContentPane().add(panel);
        frame.pack();
        return frame;
    }

    /**
     * Set the title of the parent frame or internal frame of the specified component.
     */
    public static void setTitle(Component component, final String title) {
        while (component != null) {
            if (component instanceof JInternalFrame) {
                ((JInternalFrame) component).setTitle(title);
            }
            if (component instanceof Frame) {
                ((Frame) component).setTitle(title);
                return;
            }
            if (component instanceof Dialog) {
                ((Dialog) component).setTitle(title);
                return;
            }
        }
    }

    /**
     * Brings up a "Ok/Cancel" dialog with no icon. This method can be invoked
     * from any thread and blocks until the user click on "Ok" or "Cancel".
     *
     * @param  owner  The parent component. Dialog will apears on top of this owner.
     * @param  dialog The dialog content to show.
     * @param  title  The title string for the dialog.
     * @return {@code true} if user clicked "Ok", {@code false} otherwise.
     */
    public static boolean showOptionDialog(final Component owner,
                                           final Object   dialog,
                                           final String    title)
    {
        return showOptionDialog(owner, dialog, title, null);
    }

    /**
     * Brings up a "Ok/Cancel/Reset" dialog with no icon. This method can be invoked
     * from any thread and blocks until the user click on "Ok" or "Cancel".
     *
     * @param  owner  The parent component. Dialog will apears on top of this owner.
     * @param  dialog The dialog content to show.
     * @param  title  The title string for the dialog.
     * @param  reset  Action to execute when user press "Reset", or {@code null}
     *                if there is no "Reset" button. If {@code reset} is an
     *                instance of {@link Action}, the button label will be set
     *                according the action's properties.
     * @return {@code true} if user clicked "Ok", {@code false} otherwise.
     */
    public static boolean showOptionDialog(final Component      owner,
                                           final Object        dialog,
                                           final String         title,
                                           final ActionListener reset)
    {
        // Delegates to Swing thread if this method is invoked from an other thread.
        if (!EventQueue.isDispatchThread()) {
            final boolean result[] = new boolean[1];
            invokeAndWait(new Runnable() {
                public void run() {
                    result[0] = showOptionDialog(owner, dialog, title, reset);
                }
            });
            return result[0];
        }
        // Constructs the buttons bar.
        Object[]    options = null;
        Object initialValue = null;
        int okChoice = JOptionPane.OK_OPTION;
        if (reset != null) {
            final Vocabulary resources = Vocabulary.getResources(owner!=null ? owner.getLocale() : null);
            final JButton button;
            if (reset instanceof Action) {
                button = new JButton((Action)reset);
            } else {
                button = new JButton(resources.getString(VocabularyKeys.RESET));
                button.addActionListener(reset);
            }
            options = new Object[] {
                resources.getString(VocabularyKeys.OK),
                resources.getString(VocabularyKeys.CANCEL),
                button
            };
            initialValue = options[okChoice=0];
        }

        // Brings ups the dialog box.
        final int choice;
        if (JOptionPane.getDesktopPaneForComponent(owner)!=null) {
            choice=JOptionPane.showInternalOptionDialog(
                    owner,                         // Composante parente
                    dialog,                        // Message
                    title,                         // Titre de la boîte de dialogue
                    JOptionPane.OK_CANCEL_OPTION,  // Boutons à placer
                    JOptionPane.PLAIN_MESSAGE,     // Type du message
                    null,                          // Icone
                    options,                       // Liste des boutons
                    initialValue);                 // Bouton par défaut
        } else {
            choice=JOptionPane.showOptionDialog(
                    owner,                         // Composante parente
                    dialog,                        // Message
                    title,                         // Titre de la boîte de dialogue
                    JOptionPane.OK_CANCEL_OPTION,  // Boutons à placer
                    JOptionPane.PLAIN_MESSAGE,     // Type du message
                    null,                          // Icone
                    options,                       // Liste des boutons
                    initialValue);                 // Bouton par défaut
        }
        return choice==okChoice;
    }

    /**
     * Brings up a message dialog with a "Ok" button. This method can be invoked
     * from any thread and blocks until the user click on "Ok".
     *
     * @param  owner   The parent component. Dialog will apears on top of this owner.
     * @param  message The dialog content to show.
     * @param  title   The title string for the dialog.
     * @param  type    The message type
     *                ({@link JOptionPane#ERROR_MESSAGE},
     *                 {@link JOptionPane#INFORMATION_MESSAGE},
     *                 {@link JOptionPane#WARNING_MESSAGE},
     *                 {@link JOptionPane#QUESTION_MESSAGE} or
     *                 {@link JOptionPane#PLAIN_MESSAGE}).
     */
    public static void showMessageDialog(final Component owner,
                                         final Object  message,
                                         final String    title,
                                         final int        type)
    {
        if (!EventQueue.isDispatchThread()) {
            invokeAndWait(new Runnable() {
                public void run() {
                    showMessageDialog(owner, message, title, type);
                }
            });
            return;
        }
        if (JOptionPane.getDesktopPaneForComponent(owner)!=null) {
            JOptionPane.showInternalMessageDialog(
                    owner,     // Composante parente
                    message,   // Message
                    title,     // Titre de la boîte de dialogue
                    type);     // Type du message
        } else {
            JOptionPane.showMessageDialog(
                    owner,     // Composante parente
                    message,   // Message
                    title,     // Titre de la boîte de dialogue
                    type);     // Type du message
        }
    }

    /**
     * Brings up a confirmation dialog with "Yes/No" buttons. This method can be
     * invoked from any thread and blocks until the user click on "Yes" or "No".
     *
     * @param  owner   The parent component. Dialog will apears on top of this owner.
     * @param  message The dialog content to show.
     * @param  title   The title string for the dialog.
     * @param  type    The message type
     *                ({@link JOptionPane#ERROR_MESSAGE},
     *                 {@link JOptionPane#INFORMATION_MESSAGE},
     *                 {@link JOptionPane#WARNING_MESSAGE},
     *                 {@link JOptionPane#QUESTION_MESSAGE} or
     *                 {@link JOptionPane#PLAIN_MESSAGE}).
     * @return {@code true} if user clicked on "Yes", {@code false} otherwise.
     */
    public static boolean showConfirmDialog(final Component owner,
                                            final Object  message,
                                            final String    title,
                                            final int        type)
    {
        if (!EventQueue.isDispatchThread()) {
            final boolean result[] = new boolean[1];
            invokeAndWait(new Runnable() {
                public void run() {
                    result[0]=showConfirmDialog(owner, message, title, type);
                }
            });
            return result[0];
        }
        final int choice;
        if (JOptionPane.getDesktopPaneForComponent(owner)!=null) {
            choice=JOptionPane.showInternalConfirmDialog(
                    owner,                     // Composante parente
                    message,                   // Message
                    title,                     // Titre de la boîte de dialogue
                    JOptionPane.YES_NO_OPTION, // Boutons à faire apparaître
                    type);                     // Type du message
        } else {
            choice=JOptionPane.showConfirmDialog(
                    owner,                     // Composante parente
                    message,                   // Message
                    title,                     // Titre de la boîte de dialogue
                    JOptionPane.YES_NO_OPTION, // Boutons à faire apparaître
                    type);                     // Type du message
        }
        return choice==JOptionPane.YES_OPTION;
    }

    /**
     * Retourne une étiquette pour la composante spécifiée.
     * Le texte de l'étiquette pourra éventuellement être
     * distribué sur plusieurs lignes.
     *
     * @param owner Composante pour laquelle on construit une étiquette.
     *              L'étiquette aura la même largeur que {@code owner}.
     * @param text  Texte à placer dans l'étiquette.
     */
    public static JComponent getMultilineLabelFor(final JComponent owner, final String text) {
        final JTextArea label=new JTextArea(text);
        final Dimension size=owner.getPreferredSize();
        size.height=label.getMaximumSize().height;
        label.setMaximumSize  (size);
        label.setWrapStyleWord(true);
        label.setLineWrap     (true);
        label.setEditable    (false);
        label.setFocusable   (false);
        label.setOpaque      (false);
        label.setBorder       (null); // Certains L&F placent une bordure.
        LookAndFeel.installColorsAndFont(label, "Label.background",
                                                "Label.foreground",
                                                "Label.font");
        return label;
    }

    /**
     * Returns the locale for the specified component, or a default one if the component
     * is not yet part of a container hierarchy.
     */
    public static Locale getLocale(final Component component) {
        if (component != null) try {
            return component.getLocale();
        } catch (IllegalComponentStateException ignore) {
            // Ignore. Will returns de default locale below.
        }
        return JComponent.getDefaultLocale();
    }

    /**
     * Causes runnable to have its run method called in the dispatch thread of
     * the event queue. This will happen after all pending events are processed.
     * The call blocks until this has happened.
     */
    public static void invokeAndWait(final Runnable runnable) {
        if (EventQueue.isDispatchThread()) {
            runnable.run();
        } else {
            try {
                EventQueue.invokeAndWait(runnable);
            } catch (InterruptedException exception) {
                // Someone don't want to let us sleep. Go back to work.
            } catch (InvocationTargetException target) {
                final Throwable exception=target.getTargetException();
                if (exception instanceof RuntimeException) {
                    throw (RuntimeException) exception;
                }
                if (exception instanceof Error) {
                    throw (Error) exception;
                }
                // Should not happen, since {@link Runnable#run} do not allow checked exception.
                throw new UndeclaredThrowableException(exception, exception.getLocalizedMessage());
            }
        }
    }
}
