/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2003-2006, Geotools Project Managment Committee (PMC)
 *    (C) 2001, Institut de Recherche pour le Développement
 *    (C) 1999, Pêches et Océans Canada
 *
 *    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;

// J2SE dependencies
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.BorderFactory;
import javax.swing.BoundedRangeModel;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDesktopPane;
import javax.swing.JDialog;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JLayeredPane;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

// Geotools dependencies
import org.geotools.resources.SwingUtilities;
import org.geotools.resources.Utilities;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.util.ProgressListener;
import org.geotools.util.SimpleInternationalString;
import org.opengis.util.InternationalString;


/**
 * Reports progress of a lengthly operation in a window. This implementation can also format
 * warnings. Its method can be invoked from any thread (it doesn't need to be the <cite>Swing</cite>
 * thread), which make it easier to use it from some background thread. Such background thread
 * should have a low priority in order to avoid delaying Swing repaint events.
 *
 * <p>&nbsp;</p>
 * <p align="center"><img src="doc-files/ProgressWindow.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/ProgressWindow.java $
 * @version $Id: ProgressWindow.java 22680 2006-11-09 22:32:27Z jgarnett $
 * @author Martin Desruisseaux
 */
public class ProgressWindow implements ProgressListener {
    /**
     * Initial with for the progress window, in pixels.
     */
    private static final int WIDTH = 360;

    /**
     * Initial height for the progress window, in pixels.
     * Increase this value is some component (e.g. the "Cancel" button) seems truncated.
     * The current value has been tested for Metal look and feel.
     */
    private static final int HEIGHT = 140;

    /**
     * The height of the text area containing the warning messages (if any).
     */
    private static final int WARNING_HEIGHT = 120;

    /**
     * Horizontal margin width, in pixels.
     */
    private static final int HMARGIN = 12;

    /**
     * Vertical margin height, in pixels.
     */
    private static final int VMARGIN = 9;

    /**
     * Amount of spaces to put in the margin of the warning messages window.
     */
    private static final int WARNING_MARGIN = 8;

    /**
     * The progress window as a {@link JDialog} or a {@link JInternalFrame},
     * depending of the parent component.
     */
    private final Component window;

    /**
     * The container where to add components like the progress bar.
     */
    private final JComponent content;

    /**
     * The progress bar. Values ranges from 0 to 100.
     */
    private final JProgressBar progressBar;

    /**
     * A description of the undergoing operation. Examples: "Reading header",
     * "Reading data", <cite>etc.</cite>
     */
    private final JLabel description;

    /**
     * The cancel button.
     */
    private final JButton cancel;
    
    /**
     * Component where to display warnings. The actual component class is {@link JTextArea}.
     * But we declare {@link JComponent} here in order to avoid class loading before needed.
     */
    private JComponent warningArea;

    /**
     * The source of the last warning message. Used in order to avoid to repeat the source
     * for all subsequent warning messages, if the source didn't changed.
     */
    private String lastSource;

    /**
     * {@code true} if the action has been canceled.
     */
    private volatile boolean canceled;

    /**
     * Creates a window for reporting progress. The window will not appears immediately.
     * It will appears only when the {@link #started} method will be invoked.
     *
     * @param parent The parent component, or {@code null} if none.
     */
    public ProgressWindow(final Component parent) {
        /*
         * Creates the window containing the components.
         */
        Dimension        parentSize;
        final Vocabulary  resources = Vocabulary.getResources(parent!=null ? parent.getLocale() : null);
        final String          title = resources.getString(VocabularyKeys.PROGRESSION);
        final JDesktopPane  desktop = JOptionPane.getDesktopPaneForComponent(parent);
        if (desktop != null) {
            final JInternalFrame frame;
            frame      = new JInternalFrame(title);
            window     = frame;
            content    = new JPanel(); // Pour avoir un fond opaque
            parentSize = desktop.getSize();
            frame.setContentPane(content);
            frame.setDefaultCloseOperation(JInternalFrame.HIDE_ON_CLOSE);
            desktop.add(frame, JLayeredPane.PALETTE_LAYER);
        } else {
            final Frame frame;
            final JDialog dialog;
            frame      = JOptionPane.getFrameForComponent(parent);
            dialog     = new JDialog(frame, title);
            window     = dialog;
            content    = (JComponent) dialog.getContentPane();
            parentSize = frame.getSize();
            if (parentSize.width==0 || parentSize.height==0) {
                parentSize = Toolkit.getDefaultToolkit().getScreenSize();
            }
            dialog.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
            dialog.setResizable(false);
        }
        window.setBounds((parentSize.width-WIDTH)/2, (parentSize.height-HEIGHT)/2, WIDTH, HEIGHT);
        /*
         * Creates the label that is going to display the undergoing operation.
         * This label is initially empty.
         */
        description = new JLabel();
        description.setHorizontalAlignment(JLabel.CENTER);
        /*
         * Creates the progress bar.
         */
        progressBar = new JProgressBar();
        progressBar.setIndeterminate(true);
        progressBar.setBorder(BorderFactory.createCompoundBorder(
                              BorderFactory.createEmptyBorder(6,9,6,9),
                              progressBar.getBorder()));
        /*
         * Creates the cancel button.
         */
        cancel = new JButton(resources.getString(VocabularyKeys.CANCEL));
        cancel.addActionListener( new ActionListener(){
            public void actionPerformed( ActionEvent e ) {
                setCanceled( true );
            }            
        });
        final Box cancelBox = Box.createHorizontalBox();
        cancelBox.add(Box.createGlue());
        cancelBox.add(cancel);
        cancelBox.add(Box.createGlue());
        cancelBox.setBorder(BorderFactory.createEmptyBorder(0, 0, 6, 0));
        /*
         * Layout the elements inside the window. An empty border is created in
         * order to put some space between the window content and the window border.
         */
        final JPanel panel = new JPanel(new GridLayout(2,1));
        panel.setBorder(BorderFactory.createCompoundBorder(
                        BorderFactory.createEmptyBorder(VMARGIN, HMARGIN, VMARGIN, HMARGIN),
                        BorderFactory.createEtchedBorder()));
        panel.add(description);
        panel.add(progressBar);
        content.setLayout(new BorderLayout());
        content.add(panel,     BorderLayout.NORTH);
        content.add(cancelBox, BorderLayout.SOUTH);
    }

    /**
     * Returns a localized string for the specified key.
     */
    private String getString(final int key) {
        return Vocabulary.getResources(window.getLocale()).getString(key);
    }

    /**
     * Returns the window title. The default title is "Progress" localized in current locale.
     */
    public String getTitle() {
        return (String) get(Caller.TITLE);
    }

    /**
     * Set the window title. A {@code null} value reset the default title.
     */
    public void setTitle(String name) {
        if (name == null) {
            name = getString(VocabularyKeys.PROGRESSION);
        }
        set(Caller.TITLE, name);
    }

    /**
     * {@inheritDoc}
     */
    public String getDescription() {
        return (String) get(Caller.LABEL);
    }

    /**
     * {@inheritDoc}
     */
    public void setDescription(final String description) {
        set(Caller.LABEL, description);
    }

    /**
     * Notifies that the operation begins. This method display the windows if it was
     * not already visible.
     */
    public void started() {
        call(Caller.STARTED);
    }

    /**
     * {@inheritDoc}
     */
    public void progress(final float percent) {
        int p=(int) percent; // round toward 0
        if (p<  0) p=  0;
        if (p>100) p=100;
        set(Caller.PROGRESS, new Integer(p));
    }

    /**
     * Notifies that the operation has finished. The window will disaspears, except
     * if it contains warning or exception stack traces.
     */
    public void complete() {
        call(Caller.COMPLETE);
    }

    /**
     * Releases any resource holds by this window. Invoking this method destroy the window.
     */
    public void dispose() {
        call(Caller.DISPOSE);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isCanceled() {
        return canceled;
    }

    /**
     * {@inheritDoc}
     */
    public void setCanceled(final boolean stop) {
        canceled = stop;
    }

    /**
     * Display a warning message under the progress bar. The text area for warning messages
     * will appears only the first time this method is invoked.
     */
    public synchronized void warningOccurred(final String source, String margin,
                                             final String warning)
    {
        final StringBuffer buffer = new StringBuffer(warning.length()+16);
        if (source != lastSource) {
            lastSource = source;
            if (warningArea != null) {
                buffer.append('\n');
            }
            buffer.append(source!=null ? source : getString(VocabularyKeys.UNTITLED));
            buffer.append('\n');
        }
        int wm = WARNING_MARGIN;
        if (margin != null) {
            margin = trim(margin);
            if (margin.length() != 0) {
                wm -= (margin.length()+3);
                buffer.append(Utilities.spaces(wm));
                buffer.append('(');
                buffer.append(margin);
                buffer.append(')');
                wm = 1;
            }
        }
        buffer.append(Utilities.spaces(wm));
        buffer.append(warning);
        if (buffer.charAt(buffer.length()-1) != '\n') {
            buffer.append('\n');
        }
        set(Caller.WARNING, buffer.toString());
    }

    /**
     * Display an exception stack trace.
     */
    public void exceptionOccurred(final Throwable exception) {
        ExceptionMonitor.show(window, exception);
    }

    /**
     * Returns the string {@code margin} without the parenthesis (if any).
     */
    private static String trim(String margin) {
        margin = margin.trim();
        int lower = 0;
        int upper = margin.length();
        while (lower<upper && margin.charAt(lower+0)=='(') lower++;
        while (lower<upper && margin.charAt(upper-1)==')') upper--;
        return margin.substring(lower, upper);
    }

    /**
     * Queries one of the components in the progress window. This method
     * doesn't need to be invoked from the <cite>Swing</cite> thread.
     *
     * @param  task The desired value as one of the {@link Caller#TITLE}
     *              or {@link Caller#LABEL} constants.
     * @return The value.
     */
    private Object get(final int task) {
        final Caller caller = new Caller(-task);
        SwingUtilities.invokeAndWait(caller);
        return caller.value;
    }

    /**
     * Sets the state of one of the components in the progress window.
     * This method doesn't need to be invoked from the <cite>Swing</cite> thread.
     *
     * @param  task  The value to change as one of the {@link Caller#TITLE}
     *               or {@link Caller#LABEL} constants.
     * @param  value The new value.
     */
    private void set(final int task, final Object value) {
        final Caller caller = new Caller(task);
        caller.value = value;
        EventQueue.invokeLater(caller);
    }

    /**
     * Invokes a <cite>Swing</cite> method without arguments.
     *
     * @param task The method to invoke: {@link Caller#STARTED} or {@link Caller#DISPOSE}.
     */
    private void call(final int task) {
        EventQueue.invokeLater(new Caller(task));
    }

    /**
     * Task to run in the <cite>Swing</cite> thread. Tasks are identified by a numeric
     * constant. The {@code get} operations have negative identifiers and are executed
     * by the {@link EventQueue#invokeAndWait} method. The {@code set} operations have
     * positive identifiers and are executed by the {@link EventQueue#invokeLater} method.
     *
     * @version $Id: ProgressWindow.java 22680 2006-11-09 22:32:27Z jgarnett $
     * @author Martin Desruisseaux
     */
    private class Caller implements Runnable {
        /** For getting or setting the window title. */
        public static final int TITLE = 1;

        /** For getting or setting the progress label. */
        public static final int LABEL = 2;

        /** For getting or setting the progress bar value. */
        public static final int PROGRESS = 3;

        /** For adding a warning message. */
        public static final int WARNING = 4;

        /** Notify that an action started. */
        public static final int STARTED = 5;

        /** Notify that an action is completed. */
        public static final int COMPLETE = 6;

        /** Notify that the window can be disposed. */
        public static final int DISPOSE = 7;

        /**
         * The task to execute, as one of the {@link #TITLE}, {@link #LABEL}, <cite>etc.</cite>
         * constants or their negative counterpart.
         */
        private final int task;

        /**
         * The value to get (negative value {@link #task}) or set (positive value {@link #task}).
         */
        public Object value;

        /**
         * Creates an action. {@code task} must be one of {@link #TITLE}, {@link #LABEL}
         * <cite>etc.</cite> constants or their negative counterpart.
         */
        public Caller(final int task) {
            this.task = task;
        }

        /**
         * Run the task.
         */
        public void run() {
            final BoundedRangeModel model = progressBar.getModel();
            switch (task) {
                case -LABEL: {
                    value = description.getText();
                    return;
                }
                case +LABEL: {
                    description.setText((String) value);
                    return;
                }
                case PROGRESS: {
                    model.setValue(((Integer) value).intValue());
                    progressBar.setIndeterminate(false);
                    return;
                }
                case STARTED: {
                    model.setRangeProperties(0, 1, 0, 100, false);
                    window.setVisible(true);
                    break; // Need further action below.
                }
                case COMPLETE: {
                    model.setRangeProperties(100, 1, 0, 100, false);
                    window.setVisible(warningArea != null);
                    cancel.setEnabled(false);
                    break; // Need further action below.
                }
            }
            /*
             * Some of the tasks above requires an action on the window, which may be a JDialog or
             * a JInternalFrame. We need to determine the window type before to apply the action.
             */
            synchronized (ProgressWindow.this) {
                if (window instanceof JDialog) {
                    final JDialog window = (JDialog) ProgressWindow.this.window;
                    switch (task) {
                        case -TITLE: {
                            value = window.getTitle();
                            return;
                        }
                        case +TITLE: {
                            window.setTitle((String) value);
                            return;
                        }
                        case STARTED: {
                            window.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
                            return;
                        }
                        case COMPLETE: {
                            window.setDefaultCloseOperation(JDialog.HIDE_ON_CLOSE);
                            return;
                        }
                        case DISPOSE: {
                            window.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
                            if (warningArea==null || !window.isVisible()) {
                                window.dispose();
                            }
                            return;
                        }
                    }
                } else {
                    final JInternalFrame window = (JInternalFrame) ProgressWindow.this.window;
                    switch (task) {
                        case -TITLE: {
                            value = window.getTitle();
                            return;
                        }
                        case +TITLE: {
                            window.setTitle((String) value);
                            return;
                        }
                        case STARTED: {
                            window.setClosable(false);
                            return;
                        }
                        case COMPLETE: {
                            window.setClosable(true);
                            return;
                        }
                        case DISPOSE: {
                            window.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
                            if (warningArea==null || !window.isVisible()) {
                                window.dispose();
                            }
                            return;
                        }
                    }
                }
                /*
                 * Si la tâche spécifiée n'est aucune des tâches énumérées ci-haut,
                 * on supposera que l'on voulait afficher un message d'avertissement.
                 */
                if (warningArea == null) {
                    final JTextArea     warningArea = new JTextArea();
                    final JScrollPane        scroll = new JScrollPane(warningArea);
                    final JPanel          namedArea = new JPanel(new BorderLayout());
                    ProgressWindow.this.warningArea = warningArea;
                    warningArea.setFont(Font.getFont("Monospaced"));
                    warningArea.setEditable(false);
                    namedArea.setBorder(BorderFactory.createEmptyBorder(0, HMARGIN, VMARGIN, HMARGIN));
                    namedArea.add(new JLabel(getString(VocabularyKeys.WARNING)), BorderLayout.NORTH);
                    namedArea.add(scroll,                                        BorderLayout.CENTER);
                    content.add(namedArea,                                       BorderLayout.CENTER);
                    if (window instanceof JDialog) {
                        final JDialog window = (JDialog) ProgressWindow.this.window;
                        window.setResizable(true);
                    } else {
                        final JInternalFrame window = (JInternalFrame) ProgressWindow.this.window;
                        window.setResizable(true);
                    }
                    window.setSize(WIDTH, HEIGHT+WARNING_HEIGHT);
                    window.setVisible(true); // Seems required in order to force relayout.
                }
                final JTextArea warningArea = (JTextArea) ProgressWindow.this.warningArea;
                warningArea.append((String) value);
            }
        }
    }
    public void setTask( InternationalString task ) {
        setDescription( task.toString() );
    }
    public InternationalString getTask() {
        return new SimpleInternationalString( getDescription() );
    }
}
