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

// Graphics and geometry
import java.awt.Rectangle;
import java.awt.Dimension;
import java.awt.Graphics2D;

// User interface
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.Window;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JTextArea;
import javax.swing.JComponent;
import javax.swing.LookAndFeel;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JDesktopPane;
import javax.swing.BorderFactory;
import javax.swing.JInternalFrame;
import javax.swing.AbstractButton;

// Events
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.EventQueue;

// Miscellaneous
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

// Resources
import org.geotools.util.logging.Logging;
import org.geotools.resources.Utilities;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.GraphicsUtilities;


/**
 * Utility which enables exception messages to be displayed in a <cite>Swing</cite> component. The
 * standard {@link java.lang.Exception} class contains methods which write the exception to the
 * error console. This {@code ExceptionMonitor} class adds static methods which make the message,
 * and eventually the exception trace, appear in a viewer component.
 *
 * <p>&nbsp;</p>
 * <p align="center"><img src="doc-files/ExceptionMonitor.png"></p>
 * <p>&nbsp;</p>
 *
 * @since 2.0
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/extension/widgets-swing/src/main/java/org/geotools/gui/swing/ExceptionMonitor.java $
 * @version $Id: ExceptionMonitor.java 27848 2007-11-12 13:10:32Z desruisseaux $
 * @author Martin Desruisseaux
 */
public final class ExceptionMonitor {
    /**
     * The creation of {@code ExceptionMonitor} class objects is forbidden.
     */
    private ExceptionMonitor() {
    }

    /**
     * Displays an error message for the specified exception. Note that this method can
     * be called from any thread (not necessarily the <cite>Swing</cite> thread).
     *
     * @param owner Component in which the exception is produced, or {@code null} if unknown.
     * @param exception Exception which has been thrown and is to be reported to the user.
     */
    public static void show(final Component owner, final Throwable exception) {
        show(owner, exception, null);
    }

    /**
     * Displays an error message for the specified exception. Note that this method can
     * be called from any thread (not necessarily the <cite>Swing</cite> thread).
     *
     * @param owner Component in which the exception is produced, or {@code null} if unknown.
     * @param exception Exception which has been thrown and is to be reported to the user.
     * @param message Message to display. If this parameter is null, then
     *        {@link Exception#getLocalizedMessage} will be called to obtain the message.
     */
    public static void show(final Component owner, final Throwable exception, final String message) {
        if (EventQueue.isDispatchThread()) {
            Pane.show(owner, exception, message);
        } else {
            final Runnable monitor = new Runnable() {
                public void run() {
                    Pane.show(owner, exception, message);
                }
            };
            try {
                EventQueue.invokeAndWait(monitor);
            } catch (InterruptedException error) {
                // Some doesn't want to let us sleep.  Back to work.
            } catch (InvocationTargetException error) {
                final Throwable e = error.getTargetException();
                if (e instanceof RuntimeException) throw (RuntimeException) e;
                if (e instanceof Error)            throw (Error)            e;
                Logging.unexpectedException("org.geotools.gui", ExceptionMonitor.class, "show", e);
            }
        }
    }

    /**
     * Writes the specified exception trace in the specified graphics context.  This method is
     * useful when an exception has occurred inside a {@link Component#paint} method and we want
     * to write it rather than leaving an empty window.
     *
     * @param graphics Graphics context in which to write exception.  The graphics context should
     *        be in its initial state (default affine transform, default colour, etc...)
     * @param widgetBounds Size of the trace which was being drawn.
     * @param exception Exception whose trace we want to write.
     */
    public static void paintStackTrace(final Graphics2D graphics,
                                       final Rectangle  widgetBounds,
                                       final Throwable  exception)
    {
        GraphicsUtilities.paintStackTrace(graphics, widgetBounds, exception);
    }

    /**
     * Class in charge of displaying any exception messages and eventually their traces.
     * The message will appear in a dialog box or in an internal window, depending on the
     * parent. <strong>Note:</strong> All methods in this class must be called in the
     * same thread as the <cite>Swing</cite> thread.
     *
     * @version $Id: ExceptionMonitor.java 27848 2007-11-12 13:10:32Z desruisseaux $
     * @author Martin Desruisseaux
     */
    private static final class Pane extends JOptionPane implements ActionListener {
        /**
         * Default width (in number of columns) of the component which displays
         * the exception message or trace.
         */
        private static final int WIDTH = 40;

        /**
         * Minimum height (in pixels) of the dialog box when it also displays the trace.
         */
        private static final int HEIGHT = 300;
    
        /**
         * Displayed dialog box.  It will be a {@link JDialog} object or a
         * {@link JInternalFrame} object.
         */
        private final Component dialog;

        /**
         * Exception to display in the dialog box. The method {@link Throwable#getLocalizedMessage}
         * will be called to obtain the message to display.
         */
        private final Throwable exception;
    
        /**
         * Box which will contain the "message" part of the constructed dialog box.  This box
         * will be expanded if the user asks to see the exception trace.  It will arrange the
         * components using {@link BorderLayout}.
         */
        private final Container message;

        /**
         * Component displaying the exception trace. Initially, this component will be null.
         * It will only be created if the trace is requested by the user.
         */
        private Component trace;

        /**
         * Indicates whether the trace is currently visible. This field's value
         * will be inverted each time the user presses the button "trace".
         */
        private boolean traceVisible;

        /**
         * Button which makes the trace appear or disappear.
         */
        private final AbstractButton traceButton;

        /**
         * Initial size of the dialog box {@link #dialog}. This information will be used to
         * return the box to its initial size when the trace disappears.
         */
        private final Dimension initialSize;

        /**
         * Resources in the user's language.
         */
        private final Vocabulary resources;

        /**
         * Constructs a pane which will display the specified error message.
         *
         * @param owner     Parent Component of the dialog box to be created.
         * @param exception Exception we want to report.
         * @param message   Message to display.
         * @param buttons   Buttons to place under the message.  These buttons
         *                  should be in the order "Debug", "Close".
         * @param resources Resources in the user's language.
         */
        private Pane(final Component owner,   final Throwable exception,
                     final Container message, final AbstractButton[] buttons,
                     final Vocabulary resources)
        {
            super(message, ERROR_MESSAGE, OK_CANCEL_OPTION, null, buttons);
            this.exception   = exception;
            this.message     = message;
            this.resources   = resources;
            this.traceButton = buttons[0];
            buttons[0].addActionListener(this);
            buttons[1].addActionListener(this);
            /*
             * Constructs the dialog box.  Automatically detects if we can use InternalFrame or if
             * we should be happy with JDialog. The exception trace will not be written immediately.
             */
            final String classname = Utilities.getShortClassName(exception);
            final String title = resources.getString(VocabularyKeys.ERROR_$1, classname);
            final JDesktopPane desktop = getDesktopPaneForComponent(owner);
            if (desktop != null) {
                final JInternalFrame dialog = createInternalFrame(desktop, title);
                desktop.setLayer(dialog, JDesktopPane.MODAL_LAYER.intValue());
                dialog.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
                dialog.setResizable(false);
                dialog.pack();
                this.dialog = dialog;
            } else {
                final JDialog dialog = createDialog(owner, title);
                dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
                dialog.setResizable(false);
                dialog.pack();
                this.dialog = dialog;
            }
            initialSize = dialog.getSize();
        }

        /**
         * Constructs and displays a dialog box which informs the user that an
         * exception has been produced.  This method should be called in the
         * same thread as the Swing thread.
         */
        public static void show(final Component owner, final Throwable exception, String message) {
            final Vocabulary resources = Vocabulary.getResources((owner != null) ? owner.getLocale() : null);
            if (message == null) {
                message = exception.getLocalizedMessage();
                if (message == null) {
                    final String classname = Utilities.getShortClassName(exception);
                    message = resources.getString(VocabularyKeys.NO_DETAILS_$1, classname);
                }
            }
            final JTextArea textArea = new JTextArea(message, 1, WIDTH);
            textArea.setEditable(false);
            textArea.setLineWrap(true);
            textArea.setWrapStyleWord(true);
            textArea.setBackground(null);
            textArea.setBorder(null); // Certain L&Fs have a border.
            /*
             * Constructs the rest of the dialog box.  The title bar will
             * contain the name of the exception class.
             */
            final JComponent messageBox = new JPanel(new BorderLayout());
            messageBox.add(textArea, BorderLayout.NORTH);
            final Pane pane = new Pane(owner, exception, messageBox, new AbstractButton[] {
                new JButton(resources.getString(VocabularyKeys.DEBUG)),
                new JButton(resources.getString(VocabularyKeys.CLOSE))
            }, resources);
            pane.dialog.setVisible(true);
        }

        /**
         * Displays the exception trace below the message. This method is
         * called automatically when the dialog box's "Debug" button is
         * pressed.  If the exception trace still hasn't been written, this
         * method will construct the necessary components once and for all.
         */
        public void actionPerformed(final ActionEvent event) {
            if (event.getSource() != traceButton) {
                dispose();
                return;
            }
            /*
             * Constructs the exception trace once and for all if it hasn't
             * already been constructed.
             */
            if (trace == null) {
                JComponent traceComponent = null;
                for (Throwable cause  = exception;
                cause != null;
                cause  = cause.getCause()) {
                    final JTextArea text = new JTextArea(1, WIDTH);
                    text.setTabSize(4);
                    text.setText(GraphicsUtilities.printStackTrace(cause));
                    text.setEditable(false);
                    final JScrollPane scroll = new JScrollPane(text);
                    if (traceComponent != null) {
                        if (!(traceComponent instanceof JTabbedPane)) {
                            String classname = Utilities.getShortClassName(exception);
                            JTabbedPane tabs = new JTabbedPane(JTabbedPane.TOP, 
                                                               JTabbedPane.SCROLL_TAB_LAYOUT);
                            tabs.addTab(classname, traceComponent);
                            traceComponent = tabs;
                        }
                        String classname = Utilities.getShortClassName(cause);
                        ((JTabbedPane) traceComponent).addTab(classname, scroll);
                    } else {
                        traceComponent = scroll;
                    }
                }
                if (traceComponent == null) {
                    // Should not happen
                    return;
                }
                traceComponent.setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createEmptyBorder(12, 0, 0, 0),
                traceComponent.getBorder()));
                trace = traceComponent;
            }
            /*
             * Inserts or hides the exception trace.  Even if the trace is 
             * hidden, it will not be destroyed if the user would like to
             * redisplay it.
             */
            traceButton.setText(resources.format(traceVisible ? VocabularyKeys.DEBUG
                                                              : VocabularyKeys.HIDE));
            traceVisible = !traceVisible;
            if (dialog instanceof Dialog) {
                ((Dialog) dialog).setResizable(traceVisible);
            } else {
                ((JInternalFrame) dialog).setResizable(traceVisible);
            }
            if (traceVisible) {
                message.add(trace, BorderLayout.CENTER);
                dialog.setSize(initialSize.width, HEIGHT);
            } else {
                message.remove(trace);
                dialog.setSize(initialSize);
            }
            dialog.validate();
        }

        /**
         * Frees up the resources used by this dialog box.  This method is
         * called when the user closes the dialog box which reported the
         * exception.
         */
        private void dispose() {
            if (dialog instanceof Window) {
                ((Window) dialog).dispose();
            } else {
                ((JInternalFrame) dialog).dispose();
            }
        }
    }

    /**
     * Display a dummy exception. This method is provided only as an easy
     * way to test the dialog appearance from the command line.
     */
    public static void main(final String[] args) {
        show(null, new Exception());
    }
}
