/*
 * Created on Mar 14, 2005
 */
package org.flexdock.docking.drag;

import java.awt.Component;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.EventListener;
import java.util.HashMap;
import java.util.Map;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.DockingStrategy;
import org.flexdock.docking.drag.effects.EffectsManager;
import org.flexdock.docking.event.DockingEvent;
import org.flexdock.docking.floating.policy.FloatPolicyManager;
import org.flexdock.event.EventManager;
import org.flexdock.util.DockingUtility;

/**
 * @author Christopher Butler
 *
 */
public class DragManager extends MouseAdapter implements MouseMotionListener {
	private static final String DRAG_CONTEXT = "DragManager.DRAG_CONTEXT";
	private static final Object LOCK = new Object();
	private static DragOperation currentDragOperation;
	
	private Dockable dockable;
	private DragPipeline pipeline;
	private boolean enabled;
	private Point dragOrigin;
	private HashMap dragContext;
	
	public static void prime() {
		// execute static initializer to preload resources
		EffectsManager.prime();
	}
	
	public DragManager(Dockable dockable) {
		this.dockable = dockable;
	}
	
	public void mousePressed(MouseEvent e) {
		if(dockable==null || dockable.getDockingProperties().isDockingEnabled()==Boolean.FALSE) 
			enabled = false;
		else {
			toggleDragContext(true);
			enabled = !isDragCanceled(dockable, e);
		}
	}
	
	public void mouseDragged(MouseEvent evt) {
		if(!enabled)
			return;

		if(dragOrigin==null)
			dragOrigin = evt.getPoint();
		
		if(pipeline==null || !pipeline.isOpen()) {
			if(passedDragThreshold(evt))
				openPipeline(evt);
			else
				evt.consume();
		}
		else
			pipeline.processDragEvent(evt);
	}
	
	private boolean passedDragThreshold(MouseEvent evt) {
		double distance = dragOrigin.distance(evt.getPoint());
		float threshold = dockable.getDockingProperties().getDragThreshold().floatValue();
		return distance > threshold;
	}
	
	private void openPipeline(MouseEvent evt) {
		DragOperation token = new DragOperation(dockable.getComponent(), dragOrigin, evt);
		token.setDragListener(this);
		// initialize listeners on the drag-source
		initializeListenerCaching(token);

		DragPipeline pipeline = new DragPipeline();
		this.pipeline = pipeline;
		pipeline.open(token);
	}

	public void mouseMoved(MouseEvent e) {
		// doesn't do anything
	}

	public void mouseReleased(MouseEvent e) {
		if(pipeline==null || dockable.getDockingProperties().isDockingEnabled()==Boolean.FALSE)
			return;

		finishDrag(dockable, pipeline.getDragToken(), e);				
		if(pipeline!=null)
			pipeline.close();
		toggleDragContext(false);
		dragOrigin = null;
		pipeline = null;
	}


	protected void finishDrag(Dockable dockable, DragOperation token, MouseEvent mouseEvt) {
		DockingStrategy docker = DockingManager.getDockingStrategy(dockable);
		DockingPort currentPort = DockingUtility.getParentDockingPort(dockable);
		DockingPort targetPort = token.getTargetPort();
		String region = token.getTargetRegion();

		// remove the listeners from the drag-source and all the old ones back in
		restoreCachedListeners(token);
		
		// issue a DockingEvent to allow any listeners the chance to cancel the operation.
		DockingEvent evt = new DockingEvent(dockable, currentPort, targetPort, DockingEvent.DROP_STARTED, mouseEvt, getDragContext());
		evt.setRegion(region);
		evt.setOverWindow(token.isOverWindow());
//		EventManager.notifyDockingMonitor(dockable, evt);
		EventManager.dispatch(evt, dockable);

		
		// attempt to complete the docking operation
		if(!evt.isConsumed())
			docker.dock(dockable, targetPort, region, token);
	}


	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	private static void initializeListenerCaching(DragOperation token) {
		// it's easier for us if we remove the MouseMostionListener associated with the dragSource 
		// before dragging, so normally we'll try to do that.  However, if developers really want to
		// keep them in there, then they can implement the Dockable interface for their dragSource and 
		// let mouseMotionListenersBlockedWhileDragging() return false
//		if (!dockableImpl.mouseMotionListenersBlockedWhileDragging())
//			return;

		Component dragSrc = token.getDragSource();
		EventListener[] cachedListeners = dragSrc.getListeners(MouseMotionListener.class);
		token.setCachedListeners(cachedListeners);
		DragManager dragListener = token.getDragListener();
		
		// remove all of the MouseMotionListeners
		for (int i = 0; i < cachedListeners.length; i++) {
			dragSrc.removeMouseMotionListener((MouseMotionListener) cachedListeners[i]);
		}
		// then, re-add the DragManager
		if(dragListener!=null)
			dragSrc.addMouseMotionListener(dragListener);
	}
	
	private static void restoreCachedListeners(DragOperation token) {
		Component dragSrc = token.getDragSource();
		EventListener[] cachedListeners = token.getCachedListeners();
		DragManager dragListener = token.getDragListener();		

		// remove the pipeline listener
		if(dragListener!=null)
			dragSrc.removeMouseMotionListener(dragListener);
			
		// now, re-add all of the original MouseMotionListeners
		for (int i = 0; i < cachedListeners.length; i++)
			dragSrc.addMouseMotionListener((MouseMotionListener) cachedListeners[i]);
	}
	
	private static boolean isDragCanceled(Dockable dockable, MouseEvent trigger) {
		DockingPort port = DockingUtility.getParentDockingPort(dockable);
		Map dragContext = getDragContext(dockable);
		DockingEvent evt = new DockingEvent(dockable, port, null, DockingEvent.DRAG_STARTED, trigger, dragContext);
		EventManager.dispatch(evt, dockable);
		return evt.isConsumed();
	}
	
	public static Map getDragContext(Dockable dockable) {
		Object obj = dockable==null? null: dockable.getClientProperty(DRAG_CONTEXT);
		return obj instanceof Map? (Map)obj: null;
	}
	
	private void toggleDragContext(boolean add) {
		if(add) {
			if(dragContext==null) {
				dragContext = new HashMap();
				dockable.putClientProperty(DRAG_CONTEXT, dragContext);
			}
		}
		else {
			if(dragContext!=null) {
				dragContext.clear();
				dragContext = null;
			}
			dockable.putClientProperty(DRAG_CONTEXT, null);
		}
	}

	private Map getDragContext() {
		return getDragContext(dockable);
	}
	
	public static boolean isFloatingAllowed(Dockable dockable) {
		return FloatPolicyManager.isFloatingAllowed(dockable);
	}
	
	public static DragOperation getCurrentDragOperation() {
		synchronized(LOCK) {
			return currentDragOperation;
		}
	}
	
	static void setCurrentDragOperation(DragOperation operation) {
		synchronized(LOCK) {
			currentDragOperation = operation;
		}		
	}
	
	
}
