/**
 * This software is released as part of the Pumpernickel project.
 * 
 * All com.pump resources in the Pumpernickel project are distributed under the
 * MIT License:
 * https://raw.githubusercontent.com/mickleness/pumpernickel/master/License.txt
 * 
 * More information about the Pumpernickel project is available here:
 * https://mickleness.github.io/pumpernickel/
 */
package com.pump.plaf;

import java.awt.Component;
import java.awt.Container;
import java.awt.event.ContainerEvent;
import java.awt.event.ContainerListener;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import java.awt.event.MouseEvent;
import java.util.Objects;

import javax.swing.event.MouseInputAdapter;
import javax.swing.tree.DefaultMutableTreeNode;

import com.pump.util.BooleanProperty;

/**
 * This keeps track of whether the mouse is over a component or any of its
 * descendants.
 */
public class DescendantMouseListener {

	/**
	 * Return a tree node for the given component and all its descendants. The
	 * user object of each node is the java.awt.Component.
	 */
	public static DefaultMutableTreeNode getContainerDescendants(
			Component component) {
		DefaultMutableTreeNode n = new DefaultMutableTreeNode(component);
		if (component instanceof Container) {
			Container c = (Container) component;
			Component[] children = c.getComponents();
			for (Component child : children) {
				DefaultMutableTreeNode childNode = getContainerDescendants(child);
				n.add(childNode);
			}
		}
		return n;
	}

	/**
	 * This updates <code>parentRollover</code> every time the mouse is over any
	 * part of the <code>component's</code> parent.
	 * <p>
	 * (This updates as the component is added to new parents.)
	 * <p>
	 * The original use case for this is a button that is added to a row of
	 * controls. When the mouse is anywhere over the row of controls: the button
	 * shows a slightly indicated icon.
	 * 
	 * @param component
	 *            the component whose parent will be monitored.
	 * @param parentRollover
	 *            the property to update.
	 */
	public static void installForParentOf(final Component component,
			final BooleanProperty parentRollover) {
		HierarchyListener h = new HierarchyListener() {
			DescendantMouseListener lastListener = null;

			@Override
			public void hierarchyChanged(HierarchyEvent e) {
				Container parent = component.getParent();
				if (lastListener == null || parent != lastListener.container) {
					if (lastListener != null) {
						lastListener.uninstall();
						lastListener = null;
					}
					if (parent != null) {
						lastListener = new DescendantMouseListener(parent,
								parentRollover);
					}
				}
			}

		};
		component.addHierarchyListener(h);
		h.hierarchyChanged(null);
	}

	Component container;
	BooleanProperty rolloverProperty;
	DefaultMutableTreeNode containerDescendants;

	HierarchyListener hierarchyListener = new HierarchyListener() {

		@Override
		public void hierarchyChanged(HierarchyEvent e) {
			refreshListeners();
		}

	};

	ContainerListener containerListener = new ContainerListener() {

		@Override
		public void componentAdded(ContainerEvent e) {
			refreshListeners();
		}

		@Override
		public void componentRemoved(ContainerEvent e) {
			refreshListeners();
		}

	};

	MouseInputAdapter mouseListener = new MouseInputAdapter() {

		@Override
		public void mouseEntered(MouseEvent e) {
			getRolloverProperty().setValue(true);
		}

		@Override
		public void mouseExited(MouseEvent e) {
			getRolloverProperty().setValue(false);
		}

		@Override
		public void mouseMoved(MouseEvent e) {
			getRolloverProperty().setValue(true);
		}

	};

	/**
	 * Create a new DescendantMouseListener.
	 * 
	 * @param container
	 *            the container this listener listens to.
	 */
	public DescendantMouseListener(Component container) {
		this(container, new BooleanProperty("rollover"));
	}

	/**
	 * Create a new DescendantMouseListener.
	 * 
	 * @param container
	 *            the container this listener listens to.
	 * @param rolloverProperty
	 *            the property object that is updated as MouseEvents come in.
	 */
	public DescendantMouseListener(Component container,
			BooleanProperty rolloverProperty) {
		Objects.requireNonNull(container);
		Objects.requireNonNull(rolloverProperty);
		this.container = container;
		this.rolloverProperty = rolloverProperty;
		refreshListeners();
	}

	/**
	 * Return true if the mouse is over the container or any of its descendants.
	 * This is shorthand for
	 * <code>getRolloverProperty().getValue().booleanValue();</code>
	 */
	public boolean isRollover() {
		return getRolloverProperty().getValue().booleanValue();
	}

	/**
	 * Return the property that reflects the current rollover state.
	 */
	public BooleanProperty getRolloverProperty() {
		return rolloverProperty;
	}

	/**
	 * Remove and/or add listeners if the component's hierarchy has changed.
	 */
	private void refreshListeners() {
		DefaultMutableTreeNode newContainerDescendants = getContainerDescendants(container);
		if (containerDescendants != null) {
			removeListeners(containerDescendants);
		}
		containerDescendants = newContainerDescendants;
		addListeners(containerDescendants);
	}

	/**
	 * Uninstall this listener so it never updates
	 * {@link #getRolloverProperty()} again. Once this method is called this
	 * object does nothing.
	 */
	public void uninstall() {
		if (containerDescendants != null) {
			removeListeners(containerDescendants);
			containerDescendants = null;
		}
	}

	/**
	 * Remove all our internal listeners from a component and its descendants.
	 */
	private void removeListeners(DefaultMutableTreeNode componentNode) {
		Component c = (Component) componentNode.getUserObject();
		c.removeHierarchyListener(hierarchyListener);
		c.removeMouseListener(mouseListener);
		c.removeMouseMotionListener(mouseListener);
		for (int a = 0; a < componentNode.getChildCount(); a++) {
			removeListeners((DefaultMutableTreeNode) componentNode
					.getChildAt(a));
		}
		if (c instanceof Container)
			((Container) c).removeContainerListener(containerListener);
	}

	/**
	 * Add all our internal listeners from a component and its descendants.
	 */
	private void addListeners(DefaultMutableTreeNode componentNode) {
		Component c = (Component) componentNode.getUserObject();
		c.addHierarchyListener(hierarchyListener);
		c.addMouseListener(mouseListener);
		c.addMouseMotionListener(mouseListener);
		for (int a = 0; a < componentNode.getChildCount(); a++) {
			addListeners((DefaultMutableTreeNode) componentNode.getChildAt(a));
		}
		if (c instanceof Container)
			((Container) c).addContainerListener(containerListener);
	}
}