/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.io;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Window;
import java.awt.event.ActionEvent;

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

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import java.net.URL;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.KeyStroke;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkEvent.EventType;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.Document;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.HTMLFrameHyperlinkEvent;

import net.sf.jaxodraw.gui.JaxoDialogs;
import net.sf.jaxodraw.util.JaxoPrefs;
import net.sf.jaxodraw.util.JaxoInfo;
import net.sf.jaxodraw.util.JaxoDictionary;
import net.sf.jaxodraw.util.JaxoLog;
import net.sf.jaxodraw.util.JaxoUtils;


/** Responsible for viewing, like the results of an export. In general, one has
 * to create temporary files that are opened with a default application and deleted
 * after the program terminates. For postscript preview, it is mandatory to
 * specify a default application in the preferences, the LaTeX preview may be
 * done with Java's internal JEditorPane.
 * @since 2.0
 */
public class JaxoPreview implements Cloneable {
    private static final JaxoDictionary LANGUAGE = new JaxoDictionary(JaxoPreview.class);
    /**
     * The user's current working directory.
     *
     * @deprecated previews should use {@link #TMP_DIR} for temporary files.
     */
    public static final String USER_DIR = JaxoInfo.LOG_DIR;

    /**
     * A directory for temporary files used for the preview.
     *
     * @since 2.0.2
     */
    public static final String TMP_DIR = JaxoInfo.LOG_DIR;

    /** On ActionEvent, if the event source is a Component, dispose
     * its window.
     */
    private static Action disposeWindowAction =
        new AbstractAction() {
            private static final long serialVersionUID = 7526471155622776147L;
            public void actionPerformed(final ActionEvent e) {
                if (e.getSource() instanceof Component) {
                    final Window w =
                        SwingUtilities.getWindowAncestor((Component) e
                            .getSource());

                    if (w != null) {
                        w.dispose();
                    }
                }
            }
        };
    private Component parent;
    private String title;

    /**
     * Constructor.
     * @param parentc Component whose Window to use a parent for dialogs.
     */
    public JaxoPreview(final Component parentc) {
        this.parent = parentc;
        this.title = JaxoInfo.VERSION;
    }

    /**
     * Parent component to use for window owners.
     * @return The parent component.
     */
    public final Component getParentComponent() {
        return parent;
    }

    /**
     * Sets the parent component.
     * @param value The parent component.
     */
    public void setParentComponent(final Component value) {
        parent = value;
    }

    /**
     * Returns a copy (clone) of this preview.
     * @return A clone of this preview.
     */
    public JaxoPreview copy() {
        try {
            return (JaxoPreview) clone();
        } catch (CloneNotSupportedException e) {
            throw new Error(e);
        }
    }

    /**
     * Title to be used for internally opened Windows. By
     * default, JaxoInfo.VERSION.
     * @return The title of the dialog.
     */
    public final String getTitle() {
        return title;
    }

    /**
     * Sets the title of this dialog.
     * @param value The title.
     */
    public void setTitle(final String value) {
        title = value;
    }

    /**
     * Show a window displaying the component.
     * @param c The component to show.
     */
    public void showComponent(final Component c) {
        showComponent(c, new JFrame());
    }

    /**
     * Display a component in a given frame.
     * Any contents of the frame's ContentPane are removed.
     *
     * @param c The component to show.
     * @param frame The frame to show the Component in.
     */
    public void showComponent(final Component c, final JFrame frame) {

        final JButton closeButton = new JButton(LANGUAGE.value("/Close"));
        closeButton.setBorder(BorderFactory.createRaisedBevelBorder());

        //closeButton.setToolTipText();
        closeButton.addActionListener(disposeWindowAction);

        final JPanel buttonPanel = new JPanel();
        buttonPanel.add(closeButton);

        installDisposeKeyStroke(buttonPanel);

        final JScrollPane s = new JScrollPane(c);

        if (!(c instanceof Scrollable)) {
            // TODO: determine good values somehow
            s.getVerticalScrollBar().setUnitIncrement(16);
            s.getHorizontalScrollBar().setUnitIncrement(16);
        }

        frame.getContentPane().removeAll();
        frame.getContentPane().add(s, BorderLayout.CENTER);
        frame.getContentPane().add(buttonPanel, BorderLayout.PAGE_END);
        frame.setTitle(JaxoDialogs.translatedWindowTitle(title));

        frame.pack();
        showDefaultFrame(frame);
    }

    /**
     * Show a window displaying the text file.
     * If there is an external editor, use that.
     *
     * @param text the text to show.
     * @param fileName The name of the temporary file that is created when
     *      the external editor is used. It is created in the user home dir.
     * @throws IOException when there is a problem writing the temporary file
     *      when the external editor is used.
     */
    public void showText(final String text, final String fileName) throws IOException {
        showText(text, fileName, new JFrame());
    }

    /**
     * Show a window displaying the text file.
     * If there is an external editor, use that.
     *
     * @param text the text to show.
     * @param fileName The name of the temporary file that is created when
     *      the external editor is used. It is created in the user home dir.
     * @param frame the frame to show the component in.
     * @throws IOException when there is a problem writing the temporary file
     *      when the external editor is used.
     */
    public void showText(final String text, final String fileName, final JFrame frame)
            throws IOException {
        final String prefEDITOR = JaxoPrefs.getStringPref(JaxoPrefs.PREF_EDITOR);

        if (prefEDITOR.length() == 0) {
            showTextInternally(text, frame);
        } else {
            final File f = new File(TMP_DIR, fileName);
            f.deleteOnExit();

            BufferedWriter output = null;

            try {
                output = new BufferedWriter(new FileWriter(f));
                output.write(text);
            } finally {
                if (output != null) {
                    try {
                        output.close();
                    } catch (IOException ex) {
                        JaxoLog.debug(ex);
                    }
                }
            }

            try {
                Runtime.getRuntime().exec(new String[]{prefEDITOR, f.toString()});
            } catch (IOException x) {
                JaxoLog.debug(x);
                JaxoDialogs.showErrorDialog(parent,
                    LANGUAGE.message("runError.textEditor%0", prefEDITOR));
            } catch (SecurityException x) {
                JaxoLog.debug(x);
                JaxoDialogs.showErrorDialog(parent,
                    LANGUAGE.message("runError.securityException%0", prefEDITOR));
            }
        }
    }

    /**
     * Show a window displaying the text file. Always use the
     * internal editor (JEditorPane).
     * @param text The text.
     */
    public void showTextInternally(final String text) {
        showTextInternally(text, new JFrame());
    }

    /**
     * Display a text file in a given frame.
     * Always use an internal editor (JEditorPane).
     * Any contents of the frame's ContentPane are removed.
     *
     * @param text The text.
     * @param frame the frame to show the component in.
     */
    public void showTextInternally(final String text, final JFrame frame) {
        final JEditorPane editorPane = new JEditorPane();
        editorPane.setEditable(false);
        editorPane.setContentType("text/plain");

        editorPane.setText(text);

        final JScrollPane scroller = new JScrollPane(editorPane);
        scroller.setPreferredSize(new Dimension(250, 145));
        scroller.setMinimumSize(new Dimension(10, 10));
        scroller.setBorder(BorderFactory.createEtchedBorder());

        final JButton closeButton = new JButton(LANGUAGE.value("/Close"));
        closeButton.setBorder(BorderFactory.createRaisedBevelBorder());

        //closeButton.setToolTipText();
        closeButton.addActionListener(disposeWindowAction);

        final JPanel buttonPanel = JaxoDialogs.newLineBoxLayoutPanel();

        buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

        buttonPanel.add(new JLabel(LANGUAGE.value("customTextEditor")));
        buttonPanel.add(Box.createHorizontalGlue());
        buttonPanel.add(closeButton);
        buttonPanel.add(Box.createHorizontalGlue());

        frame.getContentPane().removeAll();
        frame.getContentPane().add(scroller, BorderLayout.CENTER);
        frame.getContentPane().add(buttonPanel, BorderLayout.PAGE_END);
        frame.setTitle(JaxoDialogs.translatedWindowTitle(title));

        frame.setSize(700, 600);
        showDefaultFrame(frame);
    }

    /**
     * Show a window displaying the given URL, allowing the user to browse.
     * Use an external editor if available, otherwise the internal editor
     * (JEditorPane).
     * @param u The URL to display.
     */
    public void browseURL(final URL u) {
        final String prefBROWSER = JaxoPrefs.getStringPref(JaxoPrefs.PREF_BROWSER);

        if (prefBROWSER.length() > 0) {
            try {
                Runtime.getRuntime().exec(new String[]{
                        prefBROWSER, u.toString()
                    });
            } catch (IOException x) {
                JaxoLog.debug(x);
                JaxoDialogs.showErrorDialog(parent,
                    LANGUAGE.message("runError.browser%0", prefBROWSER));
            } catch (SecurityException x) {
                JaxoLog.debug(x);
                JaxoDialogs.showErrorDialog(parent,
                    LANGUAGE.message("runError.securityException%0", prefBROWSER));
            }
        } else {
            browseURLInternally(u);
        }
    }

    /**
     * Show a window displaying the URL (typically HTML), <strong>not</strong>
     * allowing the user to browse.
     * @param u The URL to display.
     */
    public void showURLInternally(final URL u) {
        showURLInternally(u, false);
    }

    /**
     * Show a window displaying the URL (typically HTML),
     * allowing the user to browse.
     * @param u The URL to display.
     */
    public void browseURLInternally(final URL u) {
        showURLInternally(u, true);
    }

    private void showURLInternally(final URL u, final boolean browsingAllowed) {
        final JFrame frame = new JFrame();

        frame.setTitle(JaxoDialogs.translatedWindowTitle(title));

        final JEditorPane editorPane = new JEditorPane();

        editorPane.addPropertyChangeListener(new PropertyChangeListener() {
                private String defaultTitle = title;

                public void propertyChange(final PropertyChangeEvent e) {
                    if (e.getPropertyName().equals("page")) {
                        String value =
                            (String) editorPane.getDocument().getProperty(Document.TitleProperty);

                        if (value == null) {
                            value = defaultTitle;
                        }

                        frame.setTitle(JaxoDialogs.translatedWindowTitle(value));
                    }
                }
            });

        editorPane.setEditable(false);

        if (browsingAllowed) {
            editorPane.addHyperlinkListener(new HTMLLinkListener());
        } else {
            final HTMLEditorKit k =
                (HTMLEditorKit) editorPane.getEditorKitForContentType(
                    "text/html");
            k.setLinkCursor(k.getDefaultCursor());
        }

        try {
            editorPane.setPage(u);
        } catch (IOException e) {
            final String message =
                LANGUAGE.message("showURL%0.openError", u);
            JaxoLog.debug(e);
            JaxoDialogs.showErrorDialog(parent, message);
        }

        final JScrollPane scroller = new JScrollPane(editorPane);
        scroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
        scroller.setPreferredSize(new Dimension(250, 145));
        scroller.setMinimumSize(new Dimension(10, 10));

        final JButton closeButton = new JButton(LANGUAGE.value("/Close"));
        closeButton.setBorder(BorderFactory.createRaisedBevelBorder());

        //closeButton.setToolTipText();
        closeButton.addActionListener(disposeWindowAction);

        final JPanel buttonPanel = JaxoDialogs.newLineBoxLayoutPanel();

        // as if FlowLayout
        buttonPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

        buttonPanel.add(new JLabel(LANGUAGE.value("customBrowser")));

        buttonPanel.add(Box.createHorizontalGlue());
        buttonPanel.add(closeButton);
        buttonPanel.add(Box.createHorizontalGlue());

        frame.getContentPane().add(scroller, BorderLayout.CENTER);
        frame.getContentPane().add(buttonPanel, BorderLayout.PAGE_END);

        frame.setSize(1000, 800);
        showDefaultFrame(frame);
    }

    /**
     * Is it possible to show postscript files?
     * @return True if a postscript viewer has been specified in the preferences.
     */
    public boolean canShowPostscript() {
        return JaxoPrefs.getStringPref(JaxoPrefs.PREF_PSVIEWER).length() > 0;
    }

    /** Show a message that the user cannot view postscript files.
     * @see #canShowPostscript
     */
    public void showMessageForPostscriptViewer() {
        JOptionPane.showMessageDialog(parent,
            LANGUAGE.value("choosePostscriptViewer"),
            JaxoDialogs.translatedWindowTitle(title),
            JOptionPane.INFORMATION_MESSAGE);
    }

    /**
     * Show the postscript file 'f', or a message that it cannot be shown.
     * @param f The ps file to show.
     * @see #canShowPostscript
     * @see #showMessageForPostscriptViewer
     */
    public void showPostscript(final File f) {
        final String prefPSVIEWER = JaxoPrefs.getStringPref(JaxoPrefs.PREF_PSVIEWER);

        if (prefPSVIEWER.length() == 0) {
            showMessageForPostscriptViewer();
        } else {
            try {
                Runtime.getRuntime().exec(new String[]{
                        prefPSVIEWER, f.toString()
                    });
            } catch (IOException x) {
                JaxoLog.debug(x);
                JaxoDialogs.showErrorDialog(parent,
                    LANGUAGE.message("runError.postscriptViewer%0", prefPSVIEWER));
            } catch (SecurityException x) {
                JaxoLog.debug(x);
                JaxoDialogs.showErrorDialog(parent,
                    LANGUAGE.message("runError.securityException%0", prefPSVIEWER));
            }
        }
    }

    /** Show frame. Set default properties except "size" and "title". */
    private void showDefaultFrame(final JFrame f) {
        f.setIconImage(JaxoUtils.newImage("frame.png"));
        f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
        f.setLocationRelativeTo(parent);
        f.setVisible(true);
    }

    /** Install KeyStroke/Action in Input-/ActionMap that disposes the
     * Window. Do NOT use on Components that may have tooltips.
     */
    private static void installDisposeKeyStroke(final JComponent c) {
        c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke
            .getKeyStroke("ESCAPE"), "dispose");
        c.getActionMap().put("dispose", disposeWindowAction);
    }

    // in fact, not really HTML-specific
    private static class HTMLLinkListener implements HyperlinkListener {
        public final void hyperlinkUpdate(final HyperlinkEvent e) {
            if (e.getEventType() == EventType.ACTIVATED) {
                final JEditorPane pane = (JEditorPane) e.getSource();

                if (e instanceof HTMLFrameHyperlinkEvent) {
                    final HTMLFrameHyperlinkEvent evt = (HTMLFrameHyperlinkEvent) e;
                    final HTMLDocument doc = (HTMLDocument) pane.getDocument();
                    doc.processHTMLFrameHyperlinkEvent(evt);
                } else {
                    try {
                        pane.setPage(e.getURL());
                    } catch (IOException t) {
                        JaxoLog.debug(t);
                    }
                }
            }
        }
    }
}
