/* ###
 * IP: GHIDRA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package help;

import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.Objects;

import javax.help.*;
import javax.swing.*;
import javax.swing.text.Document;

import generic.theme.GIcon;
import ghidra.util.Msg;
import ghidra.util.Swing;
import ghidra.util.bean.GGlassPane;
import resources.Icons;
import resources.MultiIconBuilder;
import resources.icons.EmptyIcon;

// NOTE: for JH 2.0, this class has been rewritten to not
// access the 'frame' and 'dialog' variable directly

/**
 * Ghidra help broker that displays the help set; sets the application icon on the help frame and
 * attempts to maintain the user window size.
 */
public class GHelpBroker extends DefaultHelpBroker {

	// Create the zoom in/out icons that will be added to the default jHelp toolbar.
	private static final Icon ZOOM_OUT_ICON = new GIcon("icon.subtract");
	private static final Icon ZOOM_IN_ICON = Icons.ADD_ICON;

	private Dimension windowSize = new Dimension(1100, 700);

	protected JEditorPane htmlEditorPane;
	private Window activationWindow;
	private boolean initialized;

	/**
	 * Construct a new GhidraHelpBroker.
	 * @param hs java help set associated with this help broker
	 */
	public GHelpBroker(HelpSet hs) {
		super(hs);
	}

	@Override
	// Overridden so that we can call the preferred version of setCurrentURL on the HelpModel,
	// which fixes a bug with the history list (SCR 7639)
	public void setCurrentURL(final URL URL) {

		HelpModel model = getCustomHelpModel();
		if (model != null) {
			model.setCurrentURL(URL, getHistoryName(URL), null);
		}
		else {
			super.setCurrentURL(URL);
		}

		Swing.runLater(() -> htmlEditorPane.requestFocusInWindow());
	}

	protected List<Image> getApplicationIcons() {
		return null;
	}

	protected HelpModel getCustomHelpModel() {
		return null;
	}

	/* Perform some shenanigans to force Java Help to reload the given URL */
	protected void reloadHelpPage(URL url, boolean preserveLocation) {

		clearContentViewer();
		showNavigationAid(url);
		try {
			// Page loading is asynchronous.  Listen for the page to be loaded and then restore the
			// users current view state.
			htmlEditorPane.addPropertyChangeListener(new PageLocationUpdater(preserveLocation));
			htmlEditorPane.setPage(url);
		}
		catch (IOException e) {
			Msg.error(this, "Unexpected error loading help page: " + url, e);
			return;
		}
	}

	private void reloadHelpPage() {
		reloadHelpPage(getCurrentURL(), true);
	}

	public void reload() {
		clearHighlights();
		initialized = false;
		if (isDisplayed()) {
			setDisplayed(false);
			setDisplayed(true);
		}
	}

	private void clearHighlights() {
		TextHelpModel helpModel = (TextHelpModel) getCustomHelpModel();
		if (helpModel != null) {
			helpModel.removeAllHighlights();
		}
	}

	protected void showNavigationAid(URL url) {
		// this base class does not have a navigation aid
	}

	private void clearContentViewer() {
		htmlEditorPane.getDocument().putProperty(Document.StreamDescriptionProperty, null);
	}

	private JEditorPane getHTMLEditorPane(JHelpContentViewer contentViewer) {
		//
		// Intimate Knowledge - construction of the viewer:
		//
		// -BorderLayout
		// -JScrollPane
		// 		-Viewport
		//      	-JHEditorPane extends JEditorPane
		//
		//
		Component[] components = contentViewer.getComponents();
		JScrollPane scrollPane = (JScrollPane) components[0];
		JViewport viewport = scrollPane.getViewport();

		return (JEditorPane) viewport.getView();
	}

	@Override
	public void setDisplayed(boolean b) {
		if (!b) {
			super.setDisplayed(b);
			return;
		}

		// this must be before any call that triggers the help system to create its window
		initializeScreenDevice();

		WindowPresentation windowPresentation = getWindowPresentation();
		updateWindowSize(windowPresentation);

		// this has to be before getHelpWindow() or the value returned will be null
		super.setDisplayed(b);

		initializeUIWindowPresentation(windowPresentation);
	}

	private void initializeScreenDevice() {
		if (initialized) {
			return;
		}

		if (activationWindow == null) {
			// This can happen when we show the 'What's New' help page on a fresh install.  In
			// that case, we were not activated from an existing window, thus, there may
			// be no parent window.
			return;
		}

		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
		GraphicsDevice[] gs = ge.getScreenDevices();
		GraphicsConfiguration config = activationWindow.getGraphicsConfiguration();
		GraphicsDevice parentDevice = config.getDevice();
		for (int i = 0; i < gs.length; i++) {
			if (gs[i] == parentDevice) {
				// update the help window's screen to match that of the parent
				setScreen(i);
			}
		}
	}

	private void initializeUIWindowPresentation(WindowPresentation windowPresentation) {

		Window helpWindow = windowPresentation.getHelpWindow();
		Container contentPane = null;
		if (helpWindow instanceof JFrame) {
			JFrame frame = (JFrame) helpWindow;
			installRootPane(frame);
			List<Image> icons = getApplicationIcons();
			if (icons != null) {
				frame.setIconImages(icons);
			}
			contentPane = frame.getContentPane();
		}
		else if (helpWindow instanceof JDialog) {
			JDialog dialog = (JDialog) helpWindow;
			installRootPane(dialog);
			contentPane = dialog.getContentPane();
		}

		initializeUIComponents(contentPane);
	}

	private void initializeUIComponents(Container contentPane) {

		if (initialized) {
			return;// already initialized
		}

		Component[] components = contentPane.getComponents();
		JHelp jHelp = (JHelp) components[0];
		JHelpContentViewer contentViewer = jHelp.getContentViewer();
		JEditorPane activeHtmlPane = getHTMLEditorPane(contentViewer);
		if (activeHtmlPane == htmlEditorPane && initialized) {
			return; // already initialized
		}

		addCustomToolbarItems(jHelp);
		htmlEditorPane = getHTMLEditorPane(contentViewer);

		// just creating the search wires everything together
		HelpModel helpModel = getCustomHelpModel();
		installHelpSearcher(jHelp, helpModel);
		if (helpModel != null) {
			installHelpSearcher(jHelp, helpModel);
		}

		installActions(jHelp);
		initialized = true;
	}

	protected void installHelpSearcher(JHelp jHelp, HelpModel helpModel) {
		// this base class does not provide an in-page search feature
	}

	/**
	 * Create zoom in/out buttons on the default help window toolbar.
	 * @param jHelp the java help object used to retrieve the help components
	 */
	protected void addCustomToolbarItems(final JHelp jHelp) {

		for (Component component : jHelp.getComponents()) {
			if (component instanceof JToolBar) {
				JToolBar toolbar = (JToolBar) component;
				toolbar.addSeparator();

				ImageIcon icon =
					new MultiIconBuilder(new EmptyIcon(24, 24)).addCenteredIcon(ZOOM_OUT_ICON)
							.build();

				Icon zoomOutIcon = icon;
				JButton zoomOutBtn = new JButton(zoomOutIcon);
				zoomOutBtn.setToolTipText("Zoom out");
				zoomOutBtn.addActionListener(e -> {
					GHelpHTMLEditorKit.zoomOut();

					// Need to reload the page to force the scroll panes to resize properly. A
					// simple revalidate/repaint won't do it.
					reloadHelpPage();
				});
				toolbar.add(zoomOutBtn);

				icon = new MultiIconBuilder(new EmptyIcon(24, 24)).addCenteredIcon(ZOOM_IN_ICON)
						.build();
				Icon zoomInIcon = icon;
				JButton zoomInBtn = new JButton(zoomInIcon);
				zoomInBtn.setToolTipText("Zoom in");
				zoomInBtn.addActionListener(e -> {
					GHelpHTMLEditorKit.zoomIn();

					// Need to reload the page to force the scroll panes to resize properly. A
					// simple revalidate/repaint won't do it.
					reloadHelpPage();
				});
				toolbar.add(zoomInBtn);

				// Once we've found the toolbar we can break out of the loop and stop looking for it.
				break;
			}
		}
	}

	protected void installActions(JHelp help) {
		// subclasses may have actions
	}

	private String getHistoryName(URL URL) {
		String text = URL.getFile();
		int index = text.lastIndexOf('/');
		if (index != -1) {
			// we want just the filename
			text = text.substring(index + 1);
		}

		String ref = URL.getRef();
		if (ref != null) {
			text += " - " + ref;
		}
		return text;
	}

	private void installRootPane(JFrame frame) {
		Component oldGlassPane = frame.getGlassPane();
		if (!(oldGlassPane instanceof GGlassPane)) {
			GGlassPane gGlassPane = new GGlassPane();
			frame.setGlassPane(gGlassPane);
			gGlassPane.setVisible(true);
		}
	}

	private void installRootPane(JDialog dialog) {
		Component oldGlassPane = dialog.getGlassPane();
		if (!(oldGlassPane instanceof GGlassPane)) {
			GGlassPane gGlassPane = new GGlassPane();
			dialog.setGlassPane(gGlassPane);
			gGlassPane.setVisible(true);
		}
	}

	private void updateWindowSize(WindowPresentation presentation) {
		if (windowSize == null) {
			return;
		}

		presentation.createHelpWindow();
		presentation.setSize(windowSize);
	}

	@Override
	public void setActivationWindow(Window window) {
		WindowPresentation windowPresentation = getWindowPresentation();
		Window helpWindow = windowPresentation.getHelpWindow();
		if (helpWindow == null) {
			activationWindow = window;
			super.setActivationWindow(window);
			return;
		}

		windowSize = helpWindow.getSize();// remember the previous size

		boolean wasModal = isModalWindow(helpWindow);
		boolean willBeModal = isModalWindow(window);
		if (!wasModal && willBeModal) {
			// in this condition, a new window will be shown, but the old one is not properly
			// closed by JavaHelp
			helpWindow.setVisible(false);
		}

		super.setActivationWindow(window);
	}

	private boolean isModalWindow(Window window) {
		if (window instanceof Dialog) {
			Dialog dialog = (Dialog) window;
			if (dialog.isModal()) {
				return true;
			}
		}
		return false;
	}

//=================================================================================================
// Inner Classes
//=================================================================================================

	private class PageLocationUpdater implements PropertyChangeListener {

		private URL url;
		private int caretPosition;
		private Rectangle viewPosition;
		private boolean preserveLocation;

		PageLocationUpdater(boolean preserveLocation) {
			this.preserveLocation = preserveLocation;
			url = getCurrentURL();
			caretPosition = htmlEditorPane.getCaretPosition();
			viewPosition = htmlEditorPane.getVisibleRect();
		}

		@Override
		public void propertyChange(PropertyChangeEvent evt) {
			String name = evt.getPropertyName();
			if (!name.equals("page")) {
				return;
			}

			htmlEditorPane.removePropertyChangeListener(this);

			URL currentUrl = getCurrentURL();
			if (!Objects.equals(currentUrl, url)) {
				return; // new page loaded; ignore
			}

			if (!preserveLocation) {
				return;
			}

			htmlEditorPane.setCaretPosition(caretPosition);
			// not sure why this needs to be done later, but setting the caret seems to trigger a
			// view updated, so try to run after that
			Swing.runLater(() -> {
				htmlEditorPane.scrollRectToVisible(viewPosition);
			});
		}
	}

}
