/*
 * Copyright (c) 2005-2010 Flamingo Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Flamingo Kirill Grouchnikov nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package org.pushingpixels.flamingo.internal.ui.common.popup;

import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.util.List;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.plaf.*;
import javax.swing.plaf.basic.ComboPopup;

import org.pushingpixels.flamingo.api.common.JCommandButton;
import org.pushingpixels.flamingo.api.common.popup.JPopupPanel;
import org.pushingpixels.flamingo.api.common.popup.PopupPanelManager;
import org.pushingpixels.flamingo.api.common.popup.PopupPanelManager.PopupEvent;
import org.pushingpixels.flamingo.api.ribbon.JRibbon;
import org.pushingpixels.flamingo.internal.ui.ribbon.JRibbonTaskToggleButton;
import org.pushingpixels.flamingo.internal.ui.ribbon.appmenu.JRibbonApplicationMenuPopupPanel;
import org.pushingpixels.flamingo.internal.utils.FlamingoUtilities;
import org.pushingpixels.flamingo.internal.utils.KeyTipManager;

/**
 * Basic UI for popup panel {@link JPopupPanel}.
 * 
 * @author Kirill Grouchnikov
 */
public class BasicPopupPanelUI extends PopupPanelUI {
	/**
	 * The associated popup panel.
	 */
	protected JPopupPanel popupPanel;

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
	 */
	public static ComponentUI createUI(JComponent c) {
		return new BasicPopupPanelUI();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#installUI(javax.swing.JComponent)
	 */
	@Override
	public void installUI(JComponent c) {
		this.popupPanel = (JPopupPanel) c;
		super.installUI(this.popupPanel);
		installDefaults();
		installComponents();
		installListeners();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#uninstallUI(javax.swing.JComponent)
	 */
	@Override
	public void uninstallUI(JComponent c) {
		uninstallListeners();
		uninstallComponents();
		uninstallDefaults();
		super.uninstallUI(this.popupPanel);
	}

	/**
	 * Installs default settings for the associated command popup menu.
	 */
	protected void installDefaults() {
		Color bg = this.popupPanel.getBackground();
		if (bg == null || bg instanceof UIResource) {
			this.popupPanel.setBackground(FlamingoUtilities.getColor(
					Color.lightGray, "PopupPanel.background",
					"Panel.background"));
		}

		Border b = this.popupPanel.getBorder();
		if (b == null || b instanceof UIResource) {
			Border toSet = UIManager.getBorder("PopupPanel.border");
			if (toSet == null)
				toSet = new BorderUIResource.CompoundBorderUIResource(
						new LineBorder(FlamingoUtilities.getBorderColor()),
						new EmptyBorder(1, 1, 1, 1));
			this.popupPanel.setBorder(toSet);
		}
		LookAndFeel.installProperty(this.popupPanel, "opaque", Boolean.TRUE);
	}

	/**
	 * Installs listeners on the associated command popup menu.
	 */
	protected void installListeners() {
		initiliazeGlobalListeners();
	}

	/**
	 * Installs components on the associated command popup menu.
	 */
	protected void installComponents() {
	}

	/**
	 * Uninstalls default settings from the associated command popup menu.
	 */
	protected void uninstallDefaults() {
		LookAndFeel.uninstallBorder(this.popupPanel);
	}

	/**
	 * Uninstalls listeners from the associated command popup menu.
	 */
	protected void uninstallListeners() {
	}

	/**
	 * Uninstalls subcomponents from the associated command popup menu.
	 */
	protected void uninstallComponents() {
	}

	/**
	 * The global listener that tracks the ESC key action on the root panes of
	 * windows that show popup panels.
	 */
	static PopupPanelManager.PopupListener popupPanelManagerListener;

	/**
	 * Initializes the global listeners.
	 */
	protected static synchronized void initiliazeGlobalListeners() {
		if (popupPanelManagerListener != null) {
			return;
		}

		popupPanelManagerListener = new PopupPanelEscapeDismisser();
		PopupPanelManager.defaultManager().addPopupListener(
				popupPanelManagerListener);

		new WindowTracker();
	}

	/**
	 * This class is used to trace the changes in the shown popup panels and
	 * install ESC key listener on the matching root pane so that the popup
	 * panels can be dismissed with the ESC key.
	 * 
	 * @author Kirill Grouchnikov
	 */
	protected static class PopupPanelEscapeDismisser implements
			PopupPanelManager.PopupListener {
		/**
		 * The currently installed action map on the {@link #tracedRootPane}.
		 */
		private ActionMap newActionMap;

		/**
		 * The currently installed input map on the {@link #tracedRootPane}.
		 */
		private InputMap newInputMap;

		/**
		 * The last shown popup panel sequence.
		 */
		List<PopupPanelManager.PopupInfo> lastPathSelected;

		/**
		 * Currently traced root pane. It is the root pane of the originating
		 * component of the first popup panel in the currently shown sequence of
		 * {@link PopupPanelManager}.
		 */
		private JRootPane tracedRootPane;

		/**
		 * Creates a new tracer for popup panels to be dismissed with ESC key.
		 */
		public PopupPanelEscapeDismisser() {
			PopupPanelManager popupPanelManager = PopupPanelManager
					.defaultManager();
			this.lastPathSelected = popupPanelManager.getShownPath();
			if (this.lastPathSelected.size() != 0) {
				traceRootPane(this.lastPathSelected);
			}
		}

		@Override
		public void popupHidden(PopupEvent event) {
			PopupPanelManager msm = PopupPanelManager.defaultManager();
			List<PopupPanelManager.PopupInfo> p = msm.getShownPath();

			if (lastPathSelected.size() != 0 && p.size() == 0) {
				// if it is the last popup panel to be dismissed, untrace the
				// root pane
				untraceRootPane();
			}

			lastPathSelected = p;
		}

		/**
		 * Removes the installed maps on the currently traced root pane.
		 */
		private void untraceRootPane() {
			if (this.tracedRootPane != null) {
				removeUIActionMap(this.tracedRootPane, this.newActionMap);
				removeUIInputMap(this.tracedRootPane, this.newInputMap);
			}
		}

		@Override
		public void popupShown(PopupEvent event) {
			PopupPanelManager msm = PopupPanelManager.defaultManager();
			List<PopupPanelManager.PopupInfo> p = msm.getShownPath();

			if (lastPathSelected.size() == 0 && p.size() != 0) {
				// if it is the first popup panel to be shown, trace the root
				// panel
				traceRootPane(p);
			}

			lastPathSelected = p;
		}

		/**
		 * Installs the maps on the root pane of the originating component of
		 * the first popup panel of the specified sequence to trace the ESC key
		 * and dismiss the shown popup panels.
		 * 
		 * @param shownPath
		 *            Popup panel sequence.
		 */
		private void traceRootPane(List<PopupPanelManager.PopupInfo> shownPath) {
			JComponent originator = shownPath.get(0).getPopupOriginator();
			this.tracedRootPane = SwingUtilities.getRootPane(originator);

			if (this.tracedRootPane != null) {
				newInputMap = new ComponentInputMapUIResource(tracedRootPane);
				newInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
						"hidePopupPanel");

				newActionMap = new ActionMapUIResource();
				newActionMap.put("hidePopupPanel", new AbstractAction() {
					@Override
					public void actionPerformed(ActionEvent e) {
						// Hide the last sequence popup for every ESC keystroke.
						// There is special case - if the keytips are shown
						// for the *second* panel of the app menu popup panel,
						// do not dismiss the popup
						List<PopupPanelManager.PopupInfo> popups = PopupPanelManager
								.defaultManager().getShownPath();
						if (popups.size() > 0) {
							PopupPanelManager.PopupInfo lastPopup = popups
									.get(popups.size() - 1);
							if (lastPopup.getPopupPanel() instanceof JRibbonApplicationMenuPopupPanel) {
								JRibbonApplicationMenuPopupPanel appMenuPopupPanel = (JRibbonApplicationMenuPopupPanel) lastPopup
										.getPopupPanel();
								KeyTipManager.KeyTipChain currentlyShownKeyTipChain = KeyTipManager
										.defaultManager()
										.getCurrentlyShownKeyTipChain();
								if ((currentlyShownKeyTipChain != null)
										&& (currentlyShownKeyTipChain.chainParentComponent == appMenuPopupPanel
												.getPanelLevel2()))
									return;
							}
						}
						PopupPanelManager.defaultManager().hideLastPopup();
					}
				});

				addUIInputMap(tracedRootPane, newInputMap);
				addUIActionMap(tracedRootPane, newActionMap);
			}
		}

		/**
		 * Adds the specified input map to the specified component.
		 * 
		 * @param c
		 *            Component.
		 * @param map
		 *            Input map to add.
		 */
		void addUIInputMap(JComponent c, InputMap map) {
			InputMap lastNonUI = null;
			InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);

			while (parent != null && !(parent instanceof UIResource)) {
				lastNonUI = parent;
				parent = parent.getParent();
			}

			if (lastNonUI == null) {
				c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map);
			} else {
				lastNonUI.setParent(map);
			}
			map.setParent(parent);
		}

		/**
		 * Adds the specified action map to the specified component.
		 * 
		 * @param c
		 *            Component.
		 * @param map
		 *            Action map to add.
		 */
		void addUIActionMap(JComponent c, ActionMap map) {
			ActionMap lastNonUI = null;
			ActionMap parent = c.getActionMap();

			while (parent != null && !(parent instanceof UIResource)) {
				lastNonUI = parent;
				parent = parent.getParent();
			}

			if (lastNonUI == null) {
				c.setActionMap(map);
			} else {
				lastNonUI.setParent(map);
			}
			map.setParent(parent);
		}

		/**
		 * Removes the specified input map from the specified component.
		 * 
		 * @param c
		 *            Component.
		 * @param map
		 *            Input map to remove.
		 */
		void removeUIInputMap(JComponent c, InputMap map) {
			InputMap im = null;
			InputMap parent = c.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);

			while (parent != null) {
				if (parent == map) {
					if (im == null) {
						c.setInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW, map
								.getParent());
					} else {
						im.setParent(map.getParent());
					}
					break;
				}
				im = parent;
				parent = parent.getParent();
			}
		}

		/**
		 * Removes the specified action map from the specified component.
		 * 
		 * @param c
		 *            Component.
		 * @param map
		 *            Action map to remove.
		 */
		void removeUIActionMap(JComponent c, ActionMap map) {
			ActionMap im = null;
			ActionMap parent = c.getActionMap();

			while (parent != null) {
				if (parent == map) {
					if (im == null) {
						c.setActionMap(map.getParent());
					} else {
						im.setParent(map.getParent());
					}
					break;
				}
				im = parent;
				parent = parent.getParent();
			}
		}
	}

	/**
	 * This class is used to dismiss popup panels on the following events:
	 * 
	 * <ul>
	 * <li>Mouse click outside any shown popup panel.</li>
	 * <li>Closing, iconifying or deactivation of a top-level window.</li>
	 * <li>Any change in the component hierarchy of a top-level window.</li>
	 * </ul>
	 * 
	 * Only one top-level window is tracked at any time. The assumption is that
	 * the {@link PopupPanelManager} only shows popup panels originating from
	 * one top-level window.
	 * 
	 * @author Kirill Grouchnikov
	 */
	protected static class WindowTracker implements
			PopupPanelManager.PopupListener, AWTEventListener,
			ComponentListener, WindowListener {

		/**
		 * The currently tracked window. It is the window of the originating
		 * component of the first popup panel in the currently shown sequence of
		 * {@link PopupPanelManager}.
		 */
		Window grabbedWindow;

		/**
		 * Last selected path in the {@link PopupPanelManager}.
		 */
		List<PopupPanelManager.PopupInfo> lastPathSelected;

		/**
		 * Creates the new window tracker.
		 */
		public WindowTracker() {
			PopupPanelManager popupPanelManager = PopupPanelManager
					.defaultManager();
			popupPanelManager.addPopupListener(this);
			this.lastPathSelected = popupPanelManager.getShownPath();
			if (this.lastPathSelected.size() != 0) {
				grabWindow(this.lastPathSelected);
			}
		}

		/**
		 * Grabs the window of the first popup panel in the specified popup
		 * panel sequence.
		 * 
		 * @param shownPath
		 *            Sequence of the currently shown popup panels.
		 */
		void grabWindow(List<PopupPanelManager.PopupInfo> shownPath) {
			final Toolkit tk = Toolkit.getDefaultToolkit();
			java.security.AccessController
					.doPrivileged(new java.security.PrivilegedAction() {
						public Object run() {
							tk.addAWTEventListener(WindowTracker.this,
									AWTEvent.MOUSE_EVENT_MASK
											| AWTEvent.MOUSE_MOTION_EVENT_MASK
											| AWTEvent.MOUSE_WHEEL_EVENT_MASK
											| AWTEvent.WINDOW_EVENT_MASK);
							return null;
						}
					});

			Component invoker = shownPath.get(0).getPopupOriginator();
			grabbedWindow = invoker instanceof Window ? (Window) invoker
					: SwingUtilities.getWindowAncestor(invoker);
			if (grabbedWindow != null) {
				grabbedWindow.addComponentListener(this);
				grabbedWindow.addWindowListener(this);
			}
		}

		/**
		 * Ungrabs the currently tracked window.
		 */
		void ungrabWindow() {
			final Toolkit tk = Toolkit.getDefaultToolkit();
			// The grab should be removed
			java.security.AccessController
					.doPrivileged(new java.security.PrivilegedAction() {
						public Object run() {
							tk.removeAWTEventListener(WindowTracker.this);
							return null;
						}
					});
			if (grabbedWindow != null) {
				grabbedWindow.removeComponentListener(this);
				grabbedWindow.removeWindowListener(this);
				grabbedWindow = null;
			}
		}

		@Override
		public void popupShown(PopupEvent event) {
			PopupPanelManager msm = PopupPanelManager.defaultManager();
			List<PopupPanelManager.PopupInfo> p = msm.getShownPath();

			if (lastPathSelected.size() == 0 && p.size() != 0) {
				// if it is the first popup panel to be shown, grab its window
				grabWindow(p);
			}

			lastPathSelected = p;
		}

		@Override
		public void popupHidden(PopupEvent event) {
			PopupPanelManager msm = PopupPanelManager.defaultManager();
			List<PopupPanelManager.PopupInfo> p = msm.getShownPath();

			if (lastPathSelected.size() != 0 && p.size() == 0) {
				// if it is the last popup panel to be hidden, ungrab its window
				ungrabWindow();
			}

			lastPathSelected = p;
		}

		public void eventDispatched(AWTEvent ev) {
			if (!(ev instanceof MouseEvent)) {
				// We are interested in MouseEvents only
				return;
			}
			MouseEvent me = (MouseEvent) ev;
			final Component src = me.getComponent();
			JPopupPanel popupPanelParent = (JPopupPanel) SwingUtilities
					.getAncestorOfClass(JPopupPanel.class, src);
			switch (me.getID()) {
			case MouseEvent.MOUSE_PRESSED:
				boolean wasCommandButtonPopupShowing = false;
				if (src instanceof JCommandButton) {
					wasCommandButtonPopupShowing = ((JCommandButton) src)
							.getPopupModel().isPopupShowing();
				}

				if (!wasCommandButtonPopupShowing && (popupPanelParent != null)) {
					// close all popups until this parent and return
					PopupPanelManager.defaultManager().hidePopups(
							popupPanelParent);
					return;
				}
				if (src instanceof JRibbonTaskToggleButton) {
					JRibbon ribbon = (JRibbon) SwingUtilities
							.getAncestorOfClass(JRibbon.class, src);
					if ((ribbon != null)
							&& FlamingoUtilities
									.isShowingMinimizedRibbonInPopup(ribbon)) {
						// This will be handled in the action listener installed
						// on ribbon task toggle buttons in BasicRibbonUI.
						// There the ribbon popup will be hidden.
						return;
					}
				}

				// if the popup of command button was showing, it will be hidden
				// in BasicCommandButtonUI.processPopupAction() - via
				// BasicCommandButtonUI.createPopupActionListener().
				if (!wasCommandButtonPopupShowing) {
					// special case - ignore mouse press on an item in a combo popup
					if (SwingUtilities
							.getAncestorOfClass(ComboPopup.class, src) == null) {
						PopupPanelManager.defaultManager().hidePopups(src);
					}
				}

				// pass the event so that it gets processed by the controls
				break;

			case MouseEvent.MOUSE_RELEASED:
				// special case - mouse release on an item in a combo popup
				if (SwingUtilities.getAncestorOfClass(ComboPopup.class, src) != null) {
					SwingUtilities.invokeLater(new Runnable() {
						@Override
						public void run() {
							PopupPanelManager.defaultManager().hidePopups(src);
						}
					});
				}

				// pass the event so that it gets processed by the controls
				break;

			case MouseEvent.MOUSE_WHEEL:
				if (popupPanelParent != null) {
					// close all popups until this parent and return
					PopupPanelManager.defaultManager().hidePopups(
							popupPanelParent);
					return;
				}

				PopupPanelManager.defaultManager().hidePopups(src);
				break;
			}
		}

		/**
		 * Checks whether the specified component lies inside a
		 * {@link JPopupPanel}.
		 * 
		 * @param src
		 *            Component.
		 * @return <code>true</code> if the specified component lies inside a
		 *         {@link JPopupPanel}.
		 */
		boolean isInPopupPanel(Component src) {
			for (Component c = src; c != null; c = c.getParent()) {
				if (c instanceof Applet || c instanceof Window) {
					break;
				} else if (c instanceof JPopupPanel) {
					return true;
				}
			}
			return false;
		}

		public void componentResized(ComponentEvent e) {
			PopupPanelManager.defaultManager().hidePopups(null);
		}

		public void componentMoved(ComponentEvent e) {
			PopupPanelManager.defaultManager().hidePopups(null);
		}

		public void componentShown(ComponentEvent e) {
			PopupPanelManager.defaultManager().hidePopups(null);
		}

		public void componentHidden(ComponentEvent e) {
			PopupPanelManager.defaultManager().hidePopups(null);
		}

		public void windowClosing(WindowEvent e) {
			PopupPanelManager.defaultManager().hidePopups(null);
		}

		public void windowClosed(WindowEvent e) {
			PopupPanelManager.defaultManager().hidePopups(null);
		}

		public void windowIconified(WindowEvent e) {
			PopupPanelManager.defaultManager().hidePopups(null);
		}

		public void windowDeactivated(WindowEvent e) {
			PopupPanelManager.defaultManager().hidePopups(null);
		}

		public void windowOpened(WindowEvent e) {
		}

		public void windowDeiconified(WindowEvent e) {
		}

		public void windowActivated(WindowEvent e) {
		}
	}
}
