/*
 * 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.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Stroke;
import java.awt.event.KeyEvent;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PLayer;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.event.PDragSequenceEventHandler;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.nodes.PPath;
import edu.umd.cs.piccolo.util.PBounds;
import edu.umd.cs.piccolo.util.PDimension;
import edu.umd.cs.piccolo.util.PNodeFilter;
import edu.umd.cs.piccolox.handles.PBoundsHandle;

/**
 * <code>PSelectionEventHandler</code> provides standard interaction for selection.  Clicking
 * selects the object under the cursor.  Shift-clicking allows multiple objects to be
 * selected.  Dragging offers marquee selection.  Pressing the delete key deletes
 * the selection by default.
 * @version 1.0
 * @author Ben Bederson
 */ 
public class PSelectionEventHandler extends PDragSequenceEventHandler {

	public static final String SELECTION_CHANGED_NOTIFICATION = "SELECTION_CHANGED_NOTIFICATION";

	final static int DASH_WIDTH = 5;
	final static int NUM_STROKES = 10;
	
	private HashMap selection = null; 		// The current selection
	private List selectableParents = null;  // List of nodes whose children can be selected
	private PPath marquee = null;
	private PNode marqueeParent = null; 	 // Node that marquee is added to as a child
	private Point2D presspt = null;
	private Point2D canvasPressPt = null;
	private float strokeNum = 0;
	private Stroke[] strokes = null;
	private HashMap allItems = null;		// Used within drag handler temporarily
	private ArrayList unselectList = null;	// Used within drag handler temporarily
	private HashMap marqueeMap = null;
	private PNode pressNode = null; 		// Node pressed on (or null if none)
	private boolean deleteKeyActive = true; // True if DELETE key should delete selection
	private Paint marqueePaint;
	private float marqueePaintTransparency = 1.0f;
	
	/**
	 * Creates a selection event handler.
	 * @param marqueeParent The node to which the event handler dynamically adds a marquee
	 * (temporarily) to represent the area being selected.
	 * @param selectableParent The node whose children will be selected
	 * by this event handler.
	 */
	public PSelectionEventHandler(PNode marqueeParent, PNode selectableParent) {
		this.marqueeParent = marqueeParent;
		this.selectableParents = new ArrayList();
		this.selectableParents.add(selectableParent);
		init();
	}

	/**
	 * Creates a selection event handler.
	 * @param marqueeParent The node to which the event handler dynamically adds a marquee
	 * (temporarily) to represent the area being selected.
	 * @param selectableParents A list of nodes whose children will be selected
	 * by this event handler.
	 */
	public PSelectionEventHandler(PNode marqueeParent, List selectableParents) {
		this.marqueeParent = marqueeParent;
		this.selectableParents = selectableParents;
		init();
	}

	protected void init() {
		float[] dash = { DASH_WIDTH, DASH_WIDTH };
		strokes = new Stroke[NUM_STROKES];
		for (int i = 0; i < NUM_STROKES; i++) {
			strokes[i] = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 1, dash, i);
		}
		
		selection = new HashMap();
		allItems = new HashMap();
		unselectList = new ArrayList();
		marqueeMap = new HashMap();
	}

	///////////////////////////////////////////////////////
	// Public static methods for manipulating the selection
	///////////////////////////////////////////////////////
		
	public void select(Collection items) {
		boolean changes = false;
		Iterator itemIt = items.iterator();
		while (itemIt.hasNext()) {
			PNode node = (PNode)itemIt.next();
			changes |= internalSelect(node);
		}
		if (changes) {
			postSelectionChanged();
		}
	}

	public void select(Map items) {
		select( items.keySet() );
	}

	private boolean internalSelect( PNode node ) {
		if (isSelected(node)) {
			return false;
		}

		selection.put(node, Boolean.TRUE);
		decorateSelectedNode(node);
		return true;
	}

	private void postSelectionChanged()
	{
		PNotificationCenter.defaultCenter().postNotification(SELECTION_CHANGED_NOTIFICATION, this);
	}

	public void select(PNode node) {
		if (internalSelect(node)) {
			postSelectionChanged();
		}
	}

	public void decorateSelectedNode(PNode node) {
		PBoundsHandle.addBoundsHandlesTo(node);
	}
		
	public void unselect(Collection items) {
		boolean changes = false;
		Iterator itemIt = items.iterator();
		while (itemIt.hasNext()) {
			PNode node = (PNode)itemIt.next();
			changes |= internalUnselect(node);
		}
		if (changes) {
			postSelectionChanged();
		}
	}

	private boolean internalUnselect( PNode node ) {
		if (!isSelected(node)) {
			return false;
		}

		undecorateSelectedNode(node);
		selection.remove(node);
		return true;
	}

	public void unselect(PNode node) {
		if( internalUnselect(node) ) {
			postSelectionChanged();
		}
	}

	public void undecorateSelectedNode(PNode node) {
		PBoundsHandle.removeBoundsHandlesFrom(node);
	}

	public void unselectAll() {
		//  Because unselect() removes from selection, we need to
		//  take a copy of it first so it isn't changed while we're iterating
		ArrayList sel = new ArrayList(selection.keySet());
		unselect( sel );
	}

	public boolean isSelected(PNode node) {
		if ((node != null) && (selection.containsKey(node))) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Returns a copy of the currently selected nodes.
	 */
	public Collection getSelection() {
		ArrayList sel = new ArrayList(selection.keySet());
		return sel;
	}

	/**
	 * Gets a reference to the currently selected nodes.  You should not modify or store
	 * this collection.
	 */
	public Collection getSelectionReference()
	{
		return Collections.unmodifiableCollection( selection.keySet() );
	}

	/**
	 * Determine if the specified node is selectable (i.e., if it is a child
	 * of the one the list of selectable parents.
	 */
	protected boolean isSelectable(PNode node) {
		boolean selectable = false;

		Iterator parentsIt = selectableParents.iterator();
		while (parentsIt.hasNext()) {
			PNode parent = (PNode)parentsIt.next();
			if (parent.getChildrenReference().contains(node)) {
				selectable = true;
				break;
			}
			else if (parent instanceof PCamera) {
				for(int i=0; i<((PCamera)parent).getLayerCount(); i++) {
					PLayer layer = ((PCamera)parent).getLayer(i);	
					if (layer.getChildrenReference().contains(node)) {
						selectable = true;
						break;	
					}
				}
			}
		}
		
		return selectable;
	}

	//////////////////////////////////////////////////////
	// Methods for modifying the set of selectable parents
	//////////////////////////////////////////////////////

	public void addSelectableParent(PNode node) {
		selectableParents.add(node);	
	}
	
	public void removeSelectableParent(PNode node) {		
		selectableParents.remove(node); 
	}
	
	public void setSelectableParent(PNode node) {
		selectableParents.clear();
		selectableParents.add(node);	
	}
	
	public void setSelectableParents(Collection c) {
		selectableParents.clear();
		selectableParents.addAll(c);	
	}

	public Collection getSelectableParents() {
		return new ArrayList(selectableParents);
	}

	////////////////////////////////////////////////////////
	// The overridden methods from PDragSequenceEventHandler
	////////////////////////////////////////////////////////
	
	protected void startDrag(PInputEvent e) {
		super.startDrag(e);

		initializeSelection(e); 			

		if (isMarqueeSelection(e)) {
			initializeMarquee(e);

			if (!isOptionSelection(e)) {
				startMarqueeSelection(e);
			}
			else {
				startOptionMarqueeSelection(e);
			}
		}
		else {					
			if (!isOptionSelection(e)) {
				startStandardSelection(e);
			} else {
				startStandardOptionSelection(e);
			}
		}
	}

	protected void drag(PInputEvent e) {
		super.drag(e);

		if (isMarqueeSelection(e)) {
			updateMarquee(e);	

			if (!isOptionSelection(e)) {
				computeMarqueeSelection(e);
			}
			else {
				computeOptionMarqueeSelection(e);
			}
		} else {
			dragStandardSelection(e);
		}
	}

	protected void endDrag(PInputEvent e) {
		super.endDrag(e);

		if (isMarqueeSelection(e)) {
			endMarqueeSelection(e); 
		}
		else {
			endStandardSelection(e);
		}		
	}

	////////////////////////////
	// Additional methods
	////////////////////////////

	public boolean isOptionSelection(PInputEvent pie) {
		return pie.isShiftDown();	
	}

	protected boolean isMarqueeSelection(PInputEvent pie) {
		return (pressNode == null); 
	}

	protected void initializeSelection(PInputEvent pie) {
		canvasPressPt = pie.getCanvasPosition();
		presspt = pie.getPosition();
		pressNode = pie.getPath().getPickedNode();
		if (pressNode instanceof PCamera) {
			pressNode = null;
		}		
	}

	protected void initializeMarquee(PInputEvent e) {
		marquee = PPath.createRectangle((float)presspt.getX(), (float)presspt.getY(), 0, 0);
		marquee.setPaint(marqueePaint);
		marquee.setTransparency(marqueePaintTransparency);
		marquee.setStrokePaint(Color.black);
		marquee.setStroke(strokes[0]);
		marqueeParent.addChild(marquee);			

		marqueeMap.clear();
	}

	protected void startOptionMarqueeSelection(PInputEvent e) { 
	}

	protected void startMarqueeSelection(PInputEvent e) {
		unselectAll();
	}
	
	protected void startStandardSelection(PInputEvent pie) {
							// Option indicator not down - clear selection, and start fresh
		if (!isSelected(pressNode)) {
			unselectAll();
			
			if (isSelectable(pressNode)) {
				select(pressNode);
			}
		}		
	}

	protected void startStandardOptionSelection(PInputEvent pie) {
							// Option indicator is down, toggle selection
		if (isSelectable(pressNode)) {
			if (isSelected(pressNode)) {
				unselect(pressNode);
			} else {
				select(pressNode);
			}
		}		
	}

	protected void updateMarquee(PInputEvent pie) {
		PBounds b = new PBounds();

		if (marqueeParent instanceof PCamera) {
			b.add(canvasPressPt);
			b.add(pie.getCanvasPosition());
		}
		else {
			b.add(presspt);
			b.add(pie.getPosition());
		}

		marquee.globalToLocal(b);
		marquee.setPathToRectangle((float) b.x, (float) b.y, (float) b.width, (float) b.height);				
		b.reset();
		b.add(presspt);
		b.add(pie.getPosition());

		allItems.clear();
		PNodeFilter filter = createNodeFilter(b);
		Iterator parentsIt = selectableParents.iterator();
		while (parentsIt.hasNext()) {
			PNode parent = (PNode) parentsIt.next();
			
			Collection items;
			if (parent instanceof PCamera) {
				items = new ArrayList();
				for(int i=0; i<((PCamera)parent).getLayerCount(); i++) {
					((PCamera)parent).getLayer(i).getAllNodes(filter,items);	
				}
			}
			else {
				items = parent.getAllNodes(filter, null);
			}
			
			Iterator itemsIt = items.iterator();
			while (itemsIt.hasNext()) {
				allItems.put(itemsIt.next(), Boolean.TRUE);
			}
		}
	}

	protected void computeMarqueeSelection(PInputEvent pie) {
		unselectList.clear();
		// Make just the items in the list selected
		// Do this efficiently by first unselecting things not in the list
		Iterator selectionEn = selection.keySet().iterator();
		while (selectionEn.hasNext()) {
			PNode node = (PNode) selectionEn.next();
			if (!allItems.containsKey(node)) {
				unselectList.add(node);
			}
		}
		unselect(unselectList);
		
		// Then select the rest
		selectionEn = allItems.keySet().iterator();
		while (selectionEn.hasNext()) {
			PNode node = (PNode) selectionEn.next();
			if (!selection.containsKey(node) && !marqueeMap.containsKey(node) && isSelectable(node)) {
				marqueeMap.put(node,Boolean.TRUE);
			}
			else if (!isSelectable(node)) {
				selectionEn.remove();
			}
		}
		
		select(allItems);		
	}

	protected void computeOptionMarqueeSelection(PInputEvent pie) {
		unselectList.clear();
		Iterator selectionEn = selection.keySet().iterator();
		while (selectionEn.hasNext()) {
			PNode node = (PNode) selectionEn.next();
			if (!allItems.containsKey(node) && marqueeMap.containsKey(node)) {
				marqueeMap.remove(node);
				unselectList.add(node);
			}
		}
		unselect(unselectList);
		

		// Then select the rest
		selectionEn = allItems.keySet().iterator();
		while (selectionEn.hasNext()) {
			PNode node = (PNode) selectionEn.next();
			if (!selection.containsKey(node) && !marqueeMap.containsKey(node) && isSelectable(node)) {
				marqueeMap.put(node,Boolean.TRUE);
			}
			else if (!isSelectable(node)) {
				selectionEn.remove();
			}
		}

		select(allItems);	
	}

	protected PNodeFilter createNodeFilter(PBounds bounds) {
		return new BoundsFilter(bounds);	
	}

	protected PBounds getMarqueeBounds() {
		if (marquee != null) {
			return marquee.getBounds();
		}	
		return new PBounds();
	}

	protected void dragStandardSelection(PInputEvent e) {
		// There was a press node, so drag selection
	    PDimension d = e.getCanvasDelta();
	    e.getTopCamera().localToView(d);

	    PDimension gDist = new PDimension();
		Iterator selectionEn = getSelection().iterator();
		while (selectionEn.hasNext()) {
			PNode node = (PNode) selectionEn.next();

			gDist.setSize(d);
			node.getParent().globalToLocal(gDist);
			node.offset(gDist.getWidth(), gDist.getHeight());
		}		
	}

	protected void endMarqueeSelection(PInputEvent e) {
		// Remove marquee
		allItems.clear();
		marqueeMap.clear();
		marquee.removeFromParent();
		marquee = null; 		
	}

	protected void endStandardSelection(PInputEvent e) {
		pressNode = null;		
	}

	/**
	 * This gets called continuously during the drag, and is used to animate the marquee
	 */
	protected void dragActivityStep(PInputEvent aEvent) {
		if (marquee != null) {
			float origStrokeNum = strokeNum;
			strokeNum = (strokeNum + 0.5f) % NUM_STROKES;	// Increment by partial steps to slow down animation
			if ((int)strokeNum != (int)origStrokeNum) {
				marquee.setStroke(strokes[(int)strokeNum]);
			}
		}
	}

	/**
	 * Delete selection when delete key is pressed (if enabled)
	 */
	public void keyPressed(PInputEvent e) {
		switch (e.getKeyCode()) {
			case KeyEvent.VK_DELETE:
				if (deleteKeyActive) {
					Iterator selectionEn = selection.keySet().iterator();
					while (selectionEn.hasNext()) {
						PNode node = (PNode) selectionEn.next();
						node.removeFromParent();
					}
					selection.clear();
				}
		}
	}

	public boolean getSupportDeleteKey() {
		return deleteKeyActive;
	}

	public boolean isDeleteKeyActive() {
		return deleteKeyActive;
	}

	/**
	 * Specifies if the DELETE key should delete the selection
	 */
	public void setDeleteKeyActive(boolean deleteKeyActive) {
		this.deleteKeyActive = deleteKeyActive;
	}

	//////////////////////
	// Inner classes
	//////////////////////

	protected class BoundsFilter implements PNodeFilter {
		PBounds localBounds = new PBounds();
		PBounds bounds;
		
		protected BoundsFilter(PBounds bounds) {
			this.bounds = bounds;
		}

		public boolean accept(PNode node) {
			localBounds.setRect(bounds);
			node.globalToLocal(localBounds);
			
			boolean boundsIntersects = node.intersects(localBounds);
			boolean isMarquee = (node == marquee);
			return (node.getPickable() && boundsIntersects && !isMarquee && !selectableParents.contains(node) && !isCameraLayer(node));
		}

		public boolean acceptChildrenOf(PNode node) {
			return selectableParents.contains(node) || isCameraLayer(node);
		}
		
		public boolean isCameraLayer(PNode node) {
			if (node instanceof PLayer) {
				for(Iterator i=selectableParents.iterator(); i.hasNext();) {
					PNode parent = (PNode)i.next();
					if (parent instanceof PCamera) {
						if (((PCamera)parent).indexOfLayer((PLayer)node) != -1) {
							return true;	
						}
					}
				}	
			}
			return false;
		}
	}

	/**
	 * Indicates the color used to paint the marquee.
	 * @return the paint for interior of the marquee
	 */
	public Paint getMarqueePaint() {
		return marqueePaint;
	}

	/**
	 * Sets the color used to paint the marquee.
	 * @param paint the paint color
	 */
	public void setMarqueePaint(Paint paint) {
		this.marqueePaint = paint;
	}

	/**
	 * Indicates the transparency level for the interior of the marquee.
	 * @return Returns the marquee paint transparency, zero to one
	 */
	public float getMarqueePaintTransparency() {
		return marqueePaintTransparency;
	}

	/**
	 * Sets the transparency level for the interior of the marquee.
	 * @param marqueePaintTransparency The marquee paint transparency to set.
	 */
	public void setMarqueePaintTransparency(float marqueePaintTransparency) {
		this.marqueePaintTransparency = marqueePaintTransparency;
	}
}