/*
 * HelpViewerDialog.java - HTML Help viewer
 * :tabSize=4:indentSize=4:noTabs=false:
 * :folding=explicit:collapseFolds=1:
 *
 * Copyright (C) 1999, 2005 Slava Pestov, Nicholas O'Leary
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package org.gjt.sp.jedit.help;

//{{{ Imports
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Toolkit;

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;


import java.awt.datatransfer.StringSelection;

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

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;

import javax.swing.SwingWorker;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;

import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTMLFrameHyperlinkEvent;
import javax.swing.text.html.HTML;

import javax.swing.text.AttributeSet;
import javax.swing.text.Element;

import org.gjt.sp.jedit.EditBus;
import org.gjt.sp.jedit.GUIUtilities;
import org.gjt.sp.jedit.jEdit;
import org.gjt.sp.jedit.MiscUtilities;

import org.gjt.sp.jedit.EditBus.EBHandler;
import org.gjt.sp.jedit.io.AutoDetection;
import org.gjt.sp.jedit.io.RegexEncodingDetector;
import org.gjt.sp.jedit.msg.PluginUpdate;

import org.gjt.sp.util.Log;

import static org.gjt.sp.jedit.help.HelpHistoryModel.HistoryEntry;
//}}}

/**
 * jEdit's searchable help viewer. It uses a Swing JEditorPane to display the HTML,
 * and implements a URL history.
 * @author Slava Pestov
 * @version $Id: HelpViewer.java 25243 2020-04-15 14:51:21Z kpouer $
 */
public class HelpViewer extends JFrame implements HelpViewerInterface, HelpHistoryModelListener
{
	private static final long serialVersionUID = 1L;
	private static final RegexEncodingDetector ENCODING_DETECTOR = new RegexEncodingDetector(":encoding=([^:]+):", "$1");

	//{{{ HelpViewer constructor
	/**
	 * Creates a new help viewer with the default help page.
	 * @since jEdit 4.0pre4
	 */
	public HelpViewer()
	{
		this("welcome.html");
	} //}}}

	//{{{ HelpViewer constructor
	/**
	 * Creates a new help viewer for the specified URL.
	 * @param url The URL
	 */
	public HelpViewer(URL url)
	{
		this(url.toString());
	} //}}}

	//{{{ HelpViewer constructor
	/**
	 * Creates a new help viewer for the specified URL.
	 * @param url The URL
	 */
	public HelpViewer(String url)
	{
		super(jEdit.getProperty("helpviewer.title"));

		setIconImage(GUIUtilities.getEditorIcon());

		try
		{
			baseURL = new File(MiscUtilities.constructPath(
				jEdit.getJEditHome(),"doc")).toURI().toURL().toString();
		}
		catch(MalformedURLException mu)
		{
			Log.log(Log.ERROR,this,mu);
			// what to do?
		}

		ActionHandler actionListener = new ActionHandler();

		JTabbedPane tabs = new JTabbedPane();
		tabs.addTab(jEdit.getProperty("helpviewer.toc.label"),
			toc = new HelpTOCPanel(this));
		tabs.addTab(jEdit.getProperty("helpviewer.search.label"),
			new HelpSearchPanel(this));
		tabs.setMinimumSize(new Dimension(0,0));

		JPanel rightPanel = new JPanel(new BorderLayout());

		Box toolBar = new Box(BoxLayout.X_AXIS);
		//toolBar.setFloatable(false);

		toolBar.add(title = new JLabel());
		toolBar.add(Box.createGlue());
		historyModel = new HelpHistoryModel(25);
		back = new HistoryButton(HistoryButton.BACK,historyModel);
		back.addActionListener(actionListener);
		toolBar.add(back);
		forward = new HistoryButton(HistoryButton.FORWARD,historyModel);
		forward.addActionListener(actionListener);
		toolBar.add(forward);
		back.setPreferredSize(forward.getPreferredSize());
		rightPanel.add(BorderLayout.NORTH,toolBar);

		viewer = new JEditorPane();
		viewer.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES,
			Boolean.TRUE);
		
		viewer.setEditable(false);
		viewer.addHyperlinkListener(new LinkHandler());
			 
		viewer.setFont(jEdit.getFontProperty("helpviewer.font"));
		viewer.addPropertyChangeListener(new PropertyChangeHandler());
		viewer.addKeyListener(new KeyHandler());
		viewer.addMouseListener(new MouseHandler());

		viewerScrollPane = new JScrollPane(viewer);

		rightPanel.add(BorderLayout.CENTER,viewerScrollPane);

		splitter = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
					  tabs,
					  rightPanel);
		splitter.setBorder(null);


		getContentPane().add(BorderLayout.CENTER,splitter);

		historyModel.addHelpHistoryModelListener(this);
		historyUpdated();

		gotoURL(url,true,0);

		setDefaultCloseOperation(DISPOSE_ON_CLOSE);

		getRootPane().setPreferredSize(new Dimension(750,500));

		pack();
		GUIUtilities.loadGeometry(this,"helpviewer");
		GUIUtilities.addSizeSaver(this,"helpviewer");

		EditBus.addToBus(this);

		setVisible(true);

		EventQueue.invokeLater(() ->
		{
			splitter.setDividerLocation(jEdit.getIntegerProperty("helpviewer.splitter",250));
			viewer.requestFocus();
		});
	} //}}}

	//{{{ gotoURL() method
	/**
	 * Displays the specified URL in the HTML component.
	 * 
	 * @param url 		 The URL
	 * @param addToHistory   Should the URL be added to the back/forward
	 * 			 history?
	 * @param scrollPosition The vertical scrollPosition
	 */
	@Override
	public void gotoURL(String url, final boolean addToHistory, final int scrollPosition)
	{
		// the TOC pane looks up user's guide URLs relative to the
		// doc directory...
		String shortURL;
		if (MiscUtilities.isURL(url))
		{
			if (url.startsWith(baseURL))
			{
				shortURL = url.substring(baseURL.length());
				if(shortURL.startsWith("/"))
				{
					shortURL = shortURL.substring(1);
				}
			}
			else
			{
				shortURL = url;
			}
		}
		else
		{
			shortURL = url;
			if(baseURL.endsWith("/"))
			{
				url = baseURL + url;
			}
			else
			{
				url = baseURL + '/' + url;
			}
		}

		// reset default cursor so that the hand cursor doesn't
		// stick around
		viewer.setCursor(Cursor.getDefaultCursor());

		try
		{
			final URL _url = new URL(url);
			final String _shortURL = shortURL;
			if(!_url.equals(viewer.getPage()))
			{
				title.setText(jEdit.getProperty("helpviewer.loading"));
			}
			else
			{
				/* don't show loading msg because we won't
				   receive a propertyChanged */
			}

			historyModel.setCurrentScrollPosition(viewer.getPage(),getCurrentScrollPosition());
			
			/* call setPage asynchronously, because it can block when
			   one can't connect to host.
			   Calling setPage outside from the EDT violates
			   the single-tread rule of Swing, but it's an experienced workaround
			   (see merge request #2984022 - fix blocking HelpViewer
			   https://sourceforge.net/tracker/?func=detail&aid=2984022&group_id=588&atid=1235750
			   for discussion).
			   Once jEdit sets JDK 7 as dependency, all this should be
			   reverted to synchronous code.
			 */
			SwingWorker<Void, Void> worker = new SwingWorker<>()
			{
				private boolean success;

				@Override
				protected Void doInBackground()
				{
					try
					{
						// reset encoding
						viewer.putClientProperty("charset", null);
						// guess encoding
						if(_url.getPath().matches(".+\\.([tT][xX][tT])"))
						{
							URLConnection connection = _url.openConnection();
							if(connection.getContentEncoding() == null)
							{
								InputStream is = connection.getInputStream();
								BufferedInputStream in = AutoDetection.getMarkedStream(is);
								String encoding = ENCODING_DETECTOR.detectEncoding(in);
								if(encoding != null)
								{
									// JEditorPane uses charset to create the reader passed to the
									// EditorKit in JEditorPane.read().
									viewer.putClientProperty("charset", encoding);
								}
								in.close();
							}
						}
						viewer.setPage(_url);
						success = true;
					}
					catch(IOException io)
					{
						Log.log(Log.ERROR,this,io);
						String[] args = { _url.toString(), io.toString() };
						GUIUtilities.error(HelpViewer.this,"read-error",args);
					}
					return null;
				}

				@Override
				protected void done()
				{
					if (success)
					{
						if (scrollPosition != 0)
						{
							viewerScrollPane.getVerticalScrollBar().setValue(scrollPosition);
						}
						if(addToHistory)
						{
							historyModel.addToHistory(_url.toString());
						}

						HelpViewer.this.shortURL = _shortURL;

						// select the appropriate tree node.
						if(_shortURL != null)
						{
							toc.selectNode(_shortURL);
						}

						viewer.requestFocus();
					}
				}
			};
			worker.execute();
		}
		catch(MalformedURLException mf)
		{
			Log.log(Log.ERROR,this,mf);
			String[] args = { url, mf.getMessage() };
			GUIUtilities.error(this,"badurl",args);
		}
	} //}}}

	//{{{ getCurrentScrollPosition() method
	int getCurrentScrollPosition() {
		return viewerScrollPane.getVerticalScrollBar().getValue();
	} //}}}

	//{{{ getCurrentPage() method
	URL getCurrentPage() {
		return viewer.getPage();
	} //}}}

	//{{{ dispose() method
	@Override
	public void dispose()
	{
		EditBus.removeFromBus(this);
		jEdit.setIntegerProperty("helpviewer.splitter",
			splitter.getDividerLocation());
		super.dispose();
	} //}}}

	//{{{ handlePluginUpdate() method
	@EBHandler
	public void handlePluginUpdate(PluginUpdate pmsg)
	{
		if(pmsg.getWhat() == PluginUpdate.LOADED
				|| pmsg.getWhat() == PluginUpdate.UNLOADED)
			{
				if(!pmsg.isExiting())
				{
					if(!queuedTOCReload)
						queueTOCReload();
					queuedTOCReload = true;
				}
			}
	} //}}}

	//{{{ getBaseURL() method
	@Override
	public String getBaseURL()
	{
		return baseURL;
	} //}}}

	//{{{ getShortURL() method
	@Override
	public String getShortURL()
	{
		return shortURL;
	} //}}}

	//{{{ historyUpdated() method
	@Override
	public void historyUpdated()
	{
		back.setEnabled(historyModel.hasPrevious());
		forward.setEnabled(historyModel.hasNext());
	} //}}}

	//{{{ getComponent method
	@Override
	public Component getComponent()
	{
		return getRootPane();
	} //}}}

	//{{{ Private members

	//{{{ Instance members
	private String baseURL;
	private String shortURL;
	private final HistoryButton back;
	private final HistoryButton forward;
	private final JEditorPane viewer;
	private final JScrollPane viewerScrollPane;
	private final JLabel title;
	private final JSplitPane splitter;
	private final HelpHistoryModel historyModel;
	private final HelpTOCPanel toc;
	private boolean queuedTOCReload;
	//}}}

	//{{{ queueTOCReload() method
	@Override
	public void queueTOCReload()
	{
		EventQueue.invokeLater(() ->
		{
			queuedTOCReload = false;
			toc.load();
		});
	} //}}}

	//}}}

	//{{{ Inner classes

	//{{{ ActionHandler class
	class ActionHandler implements ActionListener
	{
		//{{{ actionPerformed() class
		@Override
		public void actionPerformed(ActionEvent evt)
		{
			Object source = evt.getSource();
			String actionCommand = evt.getActionCommand();
			int separatorPosition = actionCommand.lastIndexOf(':');
			String url;
			int scrollPosition;
			if (-1 == separatorPosition)
			{
				url = actionCommand;
				scrollPosition = 0;
			}
			else
			{
				url = actionCommand.substring(0,separatorPosition);
				scrollPosition = Integer.parseInt(actionCommand.substring(separatorPosition+1));
			}
			if (!url.isEmpty())
			{
				gotoURL(url,false,scrollPosition);
				return;
			}

			if(source == back)
			{
				HistoryEntry entry = historyModel.back(HelpViewer.this);
				if(entry == null)
				{
					javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null); 
				}
				else
				{
					gotoURL(entry.url,false,entry.scrollPosition);
				}
			}
			else if(source == forward)
			{
				HistoryEntry entry = historyModel.forward(HelpViewer.this);
				if(entry == null)
				{
					javax.swing.UIManager.getLookAndFeel().provideErrorFeedback(null); 
				}
				else
				{
					gotoURL(entry.url,false,entry.scrollPosition);
				}
			}
		} //}}}
	} //}}}

	//{{{ LinkHandler class
	class LinkHandler implements HyperlinkListener
	{
		//{{{ hyperlinkUpdate() method
		@Override
		public void hyperlinkUpdate(HyperlinkEvent evt)
		{
			if(evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
			{
				if(evt instanceof HTMLFrameHyperlinkEvent)
				{
					((HTMLDocument)viewer.getDocument())
						.processHTMLFrameHyperlinkEvent(
						(HTMLFrameHyperlinkEvent)evt);
					historyUpdated();
				}
				else
				{
					URL url = evt.getURL();
					if(url != null)
					{
						gotoURL(url.toString(),true,0);
					}
				}
			}
			else if (evt.getEventType() == HyperlinkEvent.EventType.ENTERED)
			{
				viewer.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
			}
			else if (evt.getEventType() == HyperlinkEvent.EventType.EXITED)
			{
				viewer.setCursor(Cursor.getDefaultCursor());
			}
		} //}}}
	} //}}}

	//{{{ PropertyChangeHandler class
	class PropertyChangeHandler implements PropertyChangeListener
	{
		@Override
		public void propertyChange(PropertyChangeEvent evt)
		{
			if("page".equals(evt.getPropertyName()))
			{
				String titleStr = (String)viewer.getDocument()
					.getProperty("title");
				if(titleStr == null)
				{
					titleStr = MiscUtilities.getFileName(
						viewer.getPage().toString());
				}
				title.setText(titleStr);
				historyModel.updateTitle(viewer.getPage().toString(),
					titleStr);
			}
		}
	} //}}}

	//{{{ KeyHandler class
	private class KeyHandler extends KeyAdapter
	{
		@Override
		public void keyPressed(KeyEvent ke)
		{
			switch (ke.getKeyCode())
			{
			case KeyEvent.VK_UP:
				JScrollBar scrollBar = viewerScrollPane.getVerticalScrollBar();
				scrollBar.setValue(scrollBar.getValue()-scrollBar.getUnitIncrement(-1));
				ke.consume();
				break;
			case KeyEvent.VK_DOWN:
				scrollBar = viewerScrollPane.getVerticalScrollBar();
				scrollBar.setValue(scrollBar.getValue()+scrollBar.getUnitIncrement(1));
				ke.consume();
				break;
			case KeyEvent.VK_LEFT:
				scrollBar = viewerScrollPane.getHorizontalScrollBar();
				scrollBar.setValue(scrollBar.getValue()-scrollBar.getUnitIncrement(-1));
				ke.consume();
				break;
			case KeyEvent.VK_RIGHT:
				scrollBar = viewerScrollPane.getHorizontalScrollBar();
				scrollBar.setValue(scrollBar.getValue()+scrollBar.getUnitIncrement(1));
				ke.consume();
				break;
			case KeyEvent.VK_HOME:
				scrollBar = viewerScrollPane.getHorizontalScrollBar();
				scrollBar.setValue(0);
				scrollBar = viewerScrollPane.getVerticalScrollBar();
				scrollBar.setValue(0);
				ke.consume();
				break;
			case KeyEvent.VK_END:
				scrollBar = viewerScrollPane.getHorizontalScrollBar();
				scrollBar.setValue(scrollBar.getMaximum());
				scrollBar = viewerScrollPane.getVerticalScrollBar();
				scrollBar.setValue(scrollBar.getMaximum());
				ke.consume();
				break;
			}
		}
	} //}}}

	//{{{ MouseHandler class
	private class MouseHandler extends MouseAdapter
	{
		@Override
		public void mousePressed(MouseEvent me)
		{
			if(me.isPopupTrigger())
			{
				handlePopupTrigger(me);
			}
		}

		@Override
		public void mouseReleased(MouseEvent me)
		{
			if(me.isPopupTrigger())
			{
				handlePopupTrigger(me);
			}
		}

		private void handlePopupTrigger(MouseEvent me)
		{
            int caret = viewer.getUI().viewToModel2D(viewer, me.getPoint(), null);
            if (caret >= 0 && viewer.getDocument() instanceof HTMLDocument)
            {
                HTMLDocument hdoc = (HTMLDocument) viewer.getDocument();
                Element elem = hdoc.getCharacterElement(caret);
                if (elem.getAttributes().getAttribute(HTML.Tag.A) != null)
                {
                    Object attribute = elem.getAttributes().getAttribute(HTML.Tag.A);
                    if (attribute instanceof AttributeSet)
                    {
                        AttributeSet set = (AttributeSet) attribute;
                        final String href = (String) set.getAttribute(HTML.Attribute.HREF);
                        if (href != null)
                        {
							JPopupMenu popup = new JPopupMenu();
							JMenuItem copy = popup.add(jEdit.getProperty("helpviewer.copy-link.label"));
							copy.addActionListener(e ->
							{
								StringSelection url = new StringSelection(href);
								Toolkit.getDefaultToolkit().getSystemClipboard().setContents(url, url);
							});
							popup.show(viewer, me.getX(), me.getY());
                        }
                    }
                }
            }
		}
	} //}}}
	//}}}
}
