/*
 * Copyright (c) 2002-@year@, University of Maryland
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * 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.
 *
 * Neither the name of the University of Maryland 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.
 *
 * Piccolo was written at the Human-Computer Interaction Laboratory www.cs.umd.edu/hcil by Jesse Grosjean
 * under the supervision of Ben Bederson. The Piccolo website is www.cs.umd.edu/hcil/piccolo.
 */
package edu.umd.cs.piccolox.event;

import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.activities.PActivity;
import edu.umd.cs.piccolo.activities.PTransformActivity;
import edu.umd.cs.piccolo.event.PBasicInputEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.event.PInputEventFilter;
import edu.umd.cs.piccolo.util.PBounds;
import edu.umd.cs.piccolo.util.PDimension;

/**
 * <b>PNavigationEventHandler</b> implements simple focus based navigation. Uses 
 * mouse button one or the arrow keys to set a new focus. Animates the canvas
 * view to keep the focus node on the screen and at 100 percent scale with minimal 
 * view movement.
 * <P>
 * @version 1.0
 * @author Jesse Grosjean
 */
public class PNavigationEventHandler extends PBasicInputEventHandler {

	public static final int NORTH = 0;
	public static final int SOUTH = 1;
	public static final int EAST = 2;
	public static final int WEST = 3;
	public static final int IN = 4;
	public static final int OUT = 5;
	
	private static Hashtable NODE_TO_GLOBAL_NODE_CENTER_MAPPING = new Hashtable();
	
	private PNode focusNode;
	private PActivity navigationActivity;
	
	public PNavigationEventHandler() {
		super();
		setEventFilter(new PInputEventFilter(InputEvent.BUTTON1_MASK)); 	
	}

	//****************************************************************
	// Focus Change Events.
	//****************************************************************
	 
	public void keyPressed(PInputEvent e) {
		PNode oldLocation = focusNode;
		
		switch (e.getKeyCode()) {
			case KeyEvent.VK_LEFT:
				moveFocusLeft(e);
				break;

			case KeyEvent.VK_RIGHT:
				moveFocusRight(e);
				break;

			case KeyEvent.VK_UP:
			case KeyEvent.VK_PAGE_UP:
				if (e.isAltDown()) {
					moveFocusOut(e);
				} else {
					moveFocusUp(e);
				}
				break;

			case KeyEvent.VK_DOWN:
			case KeyEvent.VK_PAGE_DOWN:
				if (e.isAltDown()) {
					moveFocusIn(e);
				} else {
					moveFocusDown(e);
				}
				break;
		}

		if (focusNode != null && oldLocation != focusNode) {
			directCameraViewToFocus(e.getCamera(), focusNode, 500);
		}		
	}
	
	public void mousePressed(PInputEvent aEvent) {
		moveFocusToMouseOver(aEvent);
		
		if (focusNode != null) {
			directCameraViewToFocus(aEvent.getCamera(), focusNode, 500);
			aEvent.getInputManager().setKeyboardFocus(aEvent.getPath());		
		}
	}
	
	//****************************************************************
	// Focus Movement - Moves the focus the specified direction. Left,
	// right, up, down mean move the focus to the closest sibling of the 
	// current focus node that exists in that direction. Move in means
	// move the focus to a child of the current focus, move out means
	// move the focus to the parent of the current focus.
	//****************************************************************
	
	public void moveFocusDown(PInputEvent e) {
		PNode n = getNeighborInDirection(SOUTH);

		if (n != null) {
			focusNode = n;
		}
	}
	
	public void moveFocusIn(PInputEvent e) {
		PNode n = getNeighborInDirection(IN);

		if (n != null) {
			focusNode = n;
		}		
	}
	
	public void moveFocusLeft(PInputEvent e) {
		PNode n = getNeighborInDirection(WEST);

		if (n != null) {
			focusNode = n;
		}
	}
	
	public void moveFocusOut(PInputEvent e) {
		PNode n = getNeighborInDirection(OUT);

		if (n != null) {
			focusNode = n;
		}		
	}
	
	public void moveFocusRight(PInputEvent e) {
		PNode n = getNeighborInDirection(EAST);

		if (n != null) {
			focusNode = n;
		}		
	}
		
	public void moveFocusUp(PInputEvent e) {
		PNode n = getNeighborInDirection(NORTH);

		if (n != null) {
			focusNode = n;
		}
	}

	public void moveFocusToMouseOver(PInputEvent e) {
		PNode focus = e.getPickedNode();
		if (!(focus instanceof PCamera)) {
			focusNode = focus;
		}
	}
	
	public PNode getNeighborInDirection(int aDirection) {
		if (focusNode == null) return null;

		NODE_TO_GLOBAL_NODE_CENTER_MAPPING.clear();

		Point2D highlightCenter = focusNode.getGlobalFullBounds().getCenter2D();
		NODE_TO_GLOBAL_NODE_CENTER_MAPPING.put(focusNode, highlightCenter);

		List l = getNeighbors();
		sortNodesByDistanceFromPoint(l, highlightCenter);

		Iterator i = l.iterator();
		while (i.hasNext()) {
			PNode each = (PNode) i.next();
			if (nodeIsNeighborInDirection(each, aDirection)) {
				return each;
			}
		}

		return null;
	}
	
	public List getNeighbors() {
		ArrayList result = new ArrayList();
		
		if (focusNode == null) return result;
		if (focusNode.getParent() == null) return result;

		PNode focusParent = focusNode.getParent();

		Iterator i = focusParent.getChildrenIterator();

		while (i.hasNext()) {
			PNode each = (PNode) i.next();
			if (each != focusNode && each.getPickable()) {
				result.add(each);
			}
		}

		result.add(focusParent);

		i = focusNode.getChildrenIterator();
		while (i.hasNext()) {
			result.add(i.next());
		}

		return result;
	}

	public boolean nodeIsNeighborInDirection(PNode aNode, int aDirection) {
		switch (aDirection) {
			case IN: {
				return aNode.isDescendentOf(focusNode);
			}

			case OUT: {
				return aNode.isAncestorOf(focusNode);
			}
			
			default: {
				if (aNode.isAncestorOf(focusNode) || aNode.isDescendentOf(focusNode)) {
					return false;
				}
			}
		}

		Point2D highlightCenter = (Point2D) NODE_TO_GLOBAL_NODE_CENTER_MAPPING.get(focusNode);
		Point2D nodeCenter = (Point2D) NODE_TO_GLOBAL_NODE_CENTER_MAPPING.get(aNode);

		double ytest1 = nodeCenter.getX() - highlightCenter.getX() + highlightCenter.getY();
		double ytest2 = -nodeCenter.getX() + highlightCenter.getX() + highlightCenter.getY();

		switch (aDirection) {
			case NORTH: {
				if (nodeCenter.getY() < highlightCenter.getY()) {
					if (nodeCenter.getY() < ytest1 && nodeCenter.getY() < ytest2) {
						return true;
					}
				}
				break;
			}

			case EAST: {
				if (nodeCenter.getX() > highlightCenter.getX()) {
					if (nodeCenter.getY() < ytest1 && nodeCenter.getY() > ytest2) {
						return true;
					}
				}
				break;
			}

			case SOUTH: {
				if (nodeCenter.getY() > highlightCenter.getY()) {
					if (nodeCenter.getY() > ytest1 && nodeCenter.getY() > ytest2) {
						return true;
					}
				}
				break;
			}
			case WEST: {
				if (nodeCenter.getX() < highlightCenter.getX()) {
					if (nodeCenter.getY() > ytest1 && nodeCenter.getY() < ytest2) {
						return true;
					}
				}
				break;
			}
		}
		return false;
	}
	
	public void sortNodesByDistanceFromPoint(List aNodesList, final Point2D aPoint) {
		Collections.sort(aNodesList, new Comparator() {
			public int compare(Object o1, Object o2) {
				PNode each1 = (PNode) o1;
				PNode each2 = (PNode) o2;
				Point2D each1Center = each1.getGlobalFullBounds().getCenter2D();
				Point2D each2Center = each2.getGlobalFullBounds().getCenter2D();

				NODE_TO_GLOBAL_NODE_CENTER_MAPPING.put(each1, each1Center);
				NODE_TO_GLOBAL_NODE_CENTER_MAPPING.put(each2, each2Center);

				double distance1 = aPoint.distance(each1Center);
				double distance2 = aPoint.distance(each2Center);

				if (distance1 < distance2) {
					return -1;
				} else if (distance1 == distance2) {
					return 0;
				} else {
					return 1;
				}
			}
		});
	}
	
	//****************************************************************
	// Canvas Movement - The canvas view is updated so that the current
	// focus remains visible on the screen at 100 percent scale.
	//****************************************************************

	protected PActivity animateCameraViewTransformTo(final PCamera aCamera, AffineTransform aTransform, int duration) {
		boolean wasOldAnimation = false;
		
		// first stop any old animations.
		if (navigationActivity != null) {
			navigationActivity.terminate();
			wasOldAnimation = true;
		}
			
		if (duration == 0) {
			aCamera.setViewTransform(aTransform);
			return null;
		}

		AffineTransform source = aCamera.getViewTransformReference();

		if (!source.equals(aTransform)) {
			navigationActivity = aCamera.animateViewToTransform(aTransform, duration);
			((PTransformActivity)navigationActivity).setSlowInSlowOut(!wasOldAnimation);
			return navigationActivity;			
		}
		
		return null;
	}

	public PActivity directCameraViewToFocus(PCamera aCamera, PNode aFocusNode, int duration) {
		focusNode = aFocusNode;
		AffineTransform originalViewTransform = aCamera.getViewTransform();

		// Scale the canvas to include
		PDimension d = new PDimension(1, 0);
		focusNode.globalToLocal(d);
		
		double scaleFactor = d.getWidth() / aCamera.getViewScale();
		Point2D scalePoint = focusNode.getGlobalFullBounds().getCenter2D();
		if (scaleFactor != 1) {
			aCamera.scaleViewAboutPoint(scaleFactor, scalePoint.getX(), scalePoint.getY());
		}
		
		// Pan the canvas to include the view bounds with minimal canvas
		// movement.
		aCamera.animateViewToPanToBounds(focusNode.getGlobalFullBounds(), 0);

		// Get rid of any white space. The canvas may be panned and
		// zoomed in to do this. But make sure not stay constrained by max
		// magnification.
		//fillViewWhiteSpace(aCamera);

		AffineTransform resultingTransform = aCamera.getViewTransform();
		aCamera.setViewTransform(originalViewTransform);

		// Animate the canvas so that it ends up with the given
		// view transform.
		return animateCameraViewTransformTo(aCamera, resultingTransform, duration);
	}

	protected void fillViewWhiteSpace(PCamera aCamera) {
		PBounds rootBounds = aCamera.getRoot().getFullBoundsReference();
		PBounds viewBounds = aCamera.getViewBounds();

		if (!rootBounds.contains(aCamera.getViewBounds())) {
			aCamera.animateViewToPanToBounds(rootBounds, 0);
			aCamera.animateViewToPanToBounds(focusNode.getGlobalFullBounds(), 0);

			// center content.
			double dx = 0;
			double dy = 0;
			viewBounds = aCamera.getViewBounds();

			if (viewBounds.getWidth() > rootBounds.getWidth()) {   // then center along x axis.
				double boundsCenterX = rootBounds.getMinX() + (rootBounds.getWidth() / 2);
				double viewBoundsCenterX = viewBounds.getMinX() + (viewBounds.getWidth() / 2);
				dx = viewBoundsCenterX - boundsCenterX;
			}

			if (viewBounds.getHeight() > rootBounds.getHeight()) { // then center along y axis.
				double boundsCenterY = rootBounds.getMinY() + (rootBounds.getHeight() / 2);
				double viewBoundsCenterY = viewBounds.getMinY() + (viewBounds.getHeight() / 2);
				dy = viewBoundsCenterY - boundsCenterY;
			}
			aCamera.translateView(dx, dy);
		}
	}
}
