
package uk.co.wingpath.gui;

import javax.swing.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import java.util.*;
import java.beans.*;
import uk.co.wingpath.util.*;
import uk.co.wingpath.event.Event;

/**
* This is a simple HTML viewer that displays an HTML page in its own JFrame.
* It's primarily intended for displaying help pages in an application.
*/
public class JavaHtmlViewer
    implements HtmlViewer
{
    private WFrame frame;
    private JEditorPane editor;
    private ArrayList<PageView> history;
    private int historyIndex;
    private int historySize;
    private Action backAction;
    private Action forwardAction;
    private JScrollPane scrollPane;
    private JScrollBar scrollBar;

    /**
    * Class used to record a view of a page.
    */
    private class PageView
    {
        private final URL url;
        private int position;

        PageView (URL url)
        {
            this.url = url;
            position = -1;
        }
    }

    /*
    * Override HTML.create to force synchronous loading of images.
    * This is a workaround for a Swing bug where scrolling to a reference
    * (i.e. where a URL contains '#anchor') doesn't go to the right place
    * if the page contains images. By default, property change listeners
    * get called before the images have been loaded, and JEditorPane seems
    * to use a property change listener to scroll to the reference.
    * See the showUrl method below for another part of this fix.
    */
    private static class MyViewFactory
        extends HTMLEditorKit.HTMLFactory
    {
        @Override
        public View create (Element elem)
        {
            View view = super.create (elem);
            if (view instanceof ImageView)
                    ((ImageView) view).setLoadsSynchronously (true);
            return view;
        }
    }

    private static class MyHTMLEditorKit
        extends HTMLEditorKit
    {
        /*
        * Force synchronous loading of images - see commant above.
        */
        @Override
        public ViewFactory getViewFactory ()
        {
            return new MyViewFactory ();
        }

        /*
        * Force synchronous loading of whole HTML document.
        * This simplifies scrolling to the previous position when the back
        * or forward button has been clicked - it can be done after the call
        * of setPage instead of in a property-change listener.
        */
        @Override
        public Document createDefaultDocument ()
        {
            Document doc = super.createDefaultDocument ();
            ((HTMLDocument) doc).setAsynchronousLoadPriority (-1);
            return doc;
        }
    }

    /**
        constructs a JavaHtmlViewer
        @param owner parent window.
    */

    public JavaHtmlViewer (WWindow owner, String title)
    {
        Event.checkIsEventDispatchThread ();
        frame = WFrame.create (owner);
        frame.setTitle (title);
        initHistory ();
        editor = new JEditorPane ();
        editor.setEditable (false);
        editor.setDragEnabled (true);

        editor.setEditorKit (new MyHTMLEditorKit ());

        editor.addHyperlinkListener (
            new HyperlinkListener ()
            {
                public void hyperlinkUpdate (HyperlinkEvent e)
                {
                    Event.checkIsEventDispatchThread ();
                    HyperlinkEvent.EventType type = e.getEventType ();
                    if (type == HyperlinkEvent.EventType.ACTIVATED)
                    {
                        final URL url = e.getURL ();
                        String prot = url.getProtocol ();
                        if (prot.equals ("jar"))
                        {
                            // Use invokeLater to work-around a Sun bug
                            // where the link itself is moved to top of
                            // window, instead of the link target being
                            // displayed.
                            EventQueue.invokeLater (
                                new Runnable ()
                                {
                                    public void run ()
                                    {
                                        view (url);
                                    }
                                });
                        }
                        else
                        {
                            try
                            {
                                Browser.browse (url.toURI ());
                            }
                            catch (URISyntaxException ex)
                            {
                            }
                        }
                    }
                }
            });
        editor.setBorder (BorderFactory.createLoweredBevelBorder ());
        scrollPane = new JScrollPane (editor);
        scrollBar = scrollPane.getVerticalScrollBar ();

        Container contentPane = frame.getContentPane ();
        contentPane.add (scrollPane);

        JToolBar toolBar = new JToolBar ();

        backAction = new AbstractAction ("Back")
            {
                public void actionPerformed (ActionEvent e)
                {
                    back ();
                }
            };
        backAction.putValue (AbstractAction.SHORT_DESCRIPTION,
            "Go back one page");
        backAction.putValue (AbstractAction.MNEMONIC_KEY, KeyEvent.VK_B);
        JButton backButton = new JButton (backAction);
        backButton.setFocusable (false);
        toolBar.add (backButton);

        forwardAction = new AbstractAction ("Forward")
            {
                public void actionPerformed (ActionEvent e)
                {
                    forward ();
                }
            };
        forwardAction.putValue (AbstractAction.SHORT_DESCRIPTION,
            "Go forward one page");
        forwardAction.putValue (AbstractAction.MNEMONIC_KEY, KeyEvent.VK_F);
        JButton forwardButton = new JButton (forwardAction);
        forwardButton.setFocusable (false);
        toolBar.add (forwardButton);

        toolBar.addSeparator ();

        AbstractAction closeAction = new AbstractAction ("Close")
            {
                public void actionPerformed (ActionEvent e)
                {
                    closeWindow ();
                }
            };
        JButton closeButton = new JButton (closeAction);
        closeButton.setFocusable (false);
        toolBar.add (closeButton);
        contentPane.add (toolBar, BorderLayout.NORTH);

        JRootPane rootPane = frame.getRootPane ();
        Gui.addShortCut (rootPane, "BACK_KEY",
            KeyEvent.VK_LEFT, InputEvent.ALT_MASK, backAction);
        Gui.addShortCut (rootPane, "FORWARD_KEY",
            KeyEvent.VK_RIGHT, InputEvent.ALT_MASK, forwardAction);
        Gui.addShortCut (rootPane, "ESCAPE_KEY", KeyEvent.VK_ESCAPE, 0,
            closeAction);
        Gui.addEnableToolTipShortCut (rootPane);

        frame.addWindowListener (new WindowAdapter ()
            {
                public void windowClosing (WindowEvent e)
                {
                    closeWindow ();
                }
            });

        frame.setSize (700, 640);
    }

    private void closeWindow ()
    {
        assert EventQueue.isDispatchThread ();
        initHistory ();
        frame.setVisible (false);
    }

    // Display the current page

    private void showUrl ()
    {
        assert EventQueue.isDispatchThread ();
        PageView pageView = history.get (historyIndex);
        URL url = pageView.url;
        final int position = pageView.position;

        try
        {
            editor.setCursor (Cursor.getPredefinedCursor (Cursor.WAIT_CURSOR));
            editor.setPage (url);
            // Swing bug workaround: even with synchronous loading of
            // HTML and images setPage apparently returns (and the
            // property-change listener is called) before loading
            // is fully completed. There seems to be something still
            // queued on the EDT that needs to be done before we can
            // successfully set the scroll bar position (or reset the
            // cursor) - hence the use of invokeLater.
            EventQueue.invokeLater (
                new Runnable ()
                {
                    public void run ()
                    {
                        if (position >= 0)
                            scrollBar.setValue (position);
                        editor.setCursor (Cursor.getDefaultCursor ());
                    }
                });
            backAction.setEnabled (historyIndex > 0);
            forwardAction.setEnabled (historyIndex < historySize - 1);
        }
        catch (Exception e)
        {
            editor.setCursor (Cursor.getDefaultCursor ());
            editor.setContentType ("text/plain");
            editor.setText ("Can't display page " + url);
        }
        frame.setExtendedState (Frame.NORMAL);
        frame.setVisible (true);
    }

    // Save the scroll position within the current page

    private void savePosition ()
    {
        assert EventQueue.isDispatchThread ();
        if (historyIndex >= 0)
        {
            int position = scrollBar.getValue ();
            history.get (historyIndex).position = position;
        }
    }

    private void initHistory ()
    {
        assert EventQueue.isDispatchThread ();
        history = new ArrayList<PageView> ();
        historyIndex = -1;
        historySize = 0;
    }

    private void back ()
    {
        assert EventQueue.isDispatchThread ();
        if (historyIndex > 0)
        {
            savePosition ();
            --historyIndex;
            showUrl ();
        }
    }

    private void forward ()
    {
        assert EventQueue.isDispatchThread ();
        if (historyIndex < historySize - 1)
        {
            savePosition ();
            ++historyIndex;
            showUrl ();
        }
    }

    /**
        Makes the viewer's frame visible and displays a page in it
        @param url URL of page to be displayed
    */

    private void view (URL url)
    {
        assert EventQueue.isDispatchThread ();
        savePosition ();
        ++historyIndex;
        historySize = historyIndex + 1;
        history.add (historyIndex, new PageView (url));
        showUrl ();
    }

    /**
    * Makes the viewer's frame visible and displays help text..
    * @param page help text page to be displayed
    * @param pos position in page
    */
    public void show (String page, String pos)
    {
        Event.checkIsEventDispatchThread ();
        try
        {
            URL url = JavaHtmlViewer.class.getClassLoader ().getResource (page);
            if (url == null)
                url = new URL (page);
            if (pos != null)
                url = new URL (url, "#" + pos);
            view (url);
        }
        catch (MalformedURLException e)
        {
            editor.setContentType ("text/plain");
            editor.setText ("Bad page '" + page + "': " +
                Exceptions.getMessage (e));
            frame.setExtendedState (Frame.NORMAL);
            frame.setVisible (true);
        }
    }
}


