/*******************************************************************************
 * Copyright (c) 2005 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.bpel.ui.util;

import java.util.Collection;
import java.util.List;

import org.eclipse.bpel.ui.editparts.FlowEditPart;
import org.eclipse.draw2d.Cursors;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.gef.AccessibleAnchorProvider;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPartViewer;
import org.eclipse.gef.commands.Command;
import org.eclipse.gef.requests.CreateConnectionRequest;
import org.eclipse.gef.requests.CreationFactory;
import org.eclipse.gef.tools.AbstractConnectionCreationTool;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Scrollable;


/**
 * This class allows an EditPart to have initial source anchor for 
 * the ConnectionCreationTool.
 */
public class BPELConnectionCreationTool extends AbstractConnectionCreationTool {

	private EditPartViewer viewer;
	private EditPart source;
	private EditPart proposedTarget;
	private boolean lastProposedTargetConnectable;
	
	public BPELConnectionCreationTool(CreationFactory factory) {
		super(factory);
		setDefaultCursor(Cursors.ARROW); // don't use the plug
//		setDisabledCursor();  TODO: need cursor with arrow and no drop 
		setFactory(factory);
		setUnloadWhenFinished(true);
	}

	@Override
	protected boolean handleKeyDown(KeyEvent event) {
		boolean ret = ourHandleKeyDown(event);
		if (getDomain().getActiveTool() != this) {
			//set.setLockOut(false);
		}
		return ret;
	}

	@Override
	protected void handleFinished() {
		super.handleFinished();
		//set.setLockOut(false);
	}

	public void setInitialAnchor(EditPart part, EditPartViewer curViewer) {
		setViewer(curViewer);
		setTargetEditPart(part);
		updateTargetRequest();
		setConnectionSource(part);
		
		Command command = getCommand();
		((CreateConnectionRequest)getTargetRequest()).setSourceEditPart(part);
		source = part;
		proposedTarget = null;
		lastProposedTargetConnectable = false;
		
		if (command != null) {
			setState(STATE_CONNECTION_STARTED);
			setCurrentCommand(command);
			viewer = getCurrentViewer();
		}
		if (isInState(STATE_CONNECTION_STARTED)) {
			updateTargetRequest();
			updateTargetUnderMouse();
			showSourceFeedback();
			showTargetFeedback();
			setCurrentCommand(getCommand());
		}
		return;
	}

	@Override
	protected boolean handleButtonDown(int button) {
		if (button == 1 && stateTransition(STATE_CONNECTION_STARTED, STATE_TERMINAL)) {
			boolean ret = handleCreateConnection();
			if (getCommand() != null)
				handleFinished();
			return ret;
		}

		if (isInState(STATE_INITIAL) && button == 1) {
			updateTargetRequest();
			updateTargetUnderMouse();
			setConnectionSource(getTargetEditPart());
			Command command = getCommand();
			((CreateConnectionRequest)getTargetRequest()).setSourceEditPart(
				getTargetEditPart());
			if (command != null) {
				setState(STATE_CONNECTION_STARTED);
				setCurrentCommand(command);
				viewer = getCurrentViewer();
			}
		}
	
		if (isInState(STATE_INITIAL) && button != 1) {
			setState(STATE_INVALID);
			handleInvalidInput();
		}
	
		if (isInState(STATE_CONNECTION_STARTED)) {
			//Fake a drag to cause feedback to be displayed immediately on mouse down.
			handleDrag();
		}
		return true;
	}

	@Override
	public void deactivate() {
		viewer = null;
		super.deactivate();
	}

	@Override
	protected boolean handleMove() {
		if (isInState(STATE_CONNECTION_STARTED) && viewer != getCurrentViewer())
			return false;
		if (isInState(STATE_CONNECTION_STARTED | STATE_INITIAL | STATE_ACCESSIBLE_DRAG_IN_PROGRESS)) {
			updateTargetRequest();
			updateTargetUnderMouse();
			showSourceFeedback();
			showTargetFeedback();
			setCurrentCommand(getCommand());
		}
		return true;
	}
	
	@Override
	protected boolean updateTargetUnderMouse() {
		if (!isTargetLocked()) {
			Collection exclude = getExclusionSet();
			EditPart editPart = getCurrentViewer().findObjectAtExcluding(
				getLocation(),
				exclude,
				getTargetingConditional());
			if (editPart != null) {
				editPart = editPart.getTargetEditPart(getTargetRequest());
			}
			boolean changed = getTargetEditPart() != editPart;
			setTargetEditPart(editPart);
			return changed;
		}
		return false;
	}
	
	@Override
	protected EditPartViewer.Conditional getTargetingConditional() {
		return new EditPartViewer.Conditional() {
			public boolean evaluate(EditPart editpart) {
				EditPart target = editpart.getTargetEditPart(getTargetRequest());
				if (target == null)
					return false;
				if (target == proposedTarget)
					return lastProposedTargetConnectable; 
				
				if (target.getParent() instanceof FlowEditPart) {
					lastProposedTargetConnectable = ((FlowEditPart)(target.getParent())).detectImpendingCycle(source, target);
				}
				else {
					lastProposedTargetConnectable = true;
				}
				proposedTarget = target;
				return lastProposedTargetConnectable;
			}
		};
	}
	
	boolean acceptConnectionFinish(KeyEvent event) {
		return isInState(STATE_ACCESSIBLE_DRAG_IN_PROGRESS | STATE_CONNECTION_STARTED) && event.character == 13;
	}

	boolean acceptConnectionStart(KeyEvent event) {
		return isInState(STATE_INITIAL) && event.character == 13;
	}

	/**
	 * Cleans up feedback and resets the tool when focus is lost.
	 * @return <code>true</code> if this focus lost event was processed
	 */
	@Override
	protected boolean handleFocusLost() {
		if (isInState(STATE_CONNECTION_STARTED | STATE_ACCESSIBLE_DRAG_IN_PROGRESS)) {
			eraseSourceFeedback();
			eraseTargetFeedback();
			setState(STATE_INVALID);
			handleFinished();
		}
		return super.handleFocusLost();
	}

	
	private boolean ourAcceptArrowKey(KeyEvent e) {
		int key = e.keyCode;
		if (!(isInState(STATE_INITIAL 
		  | STATE_CONNECTION_STARTED  
		  | STATE_ACCESSIBLE_DRAG
		  | STATE_ACCESSIBLE_DRAG_IN_PROGRESS)))
			return false;
		return (key == SWT.ARROW_UP)
			|| (key == SWT.ARROW_RIGHT)
			|| (key == SWT.ARROW_DOWN)
			|| (key == SWT.ARROW_LEFT);
	}
	
	private boolean ourHandleKeyDown(KeyEvent event) {
		if (ourAcceptArrowKey(event)) {
			int direction = 0;
			switch (event.keyCode) {
				case SWT.ARROW_DOWN :
					direction = PositionConstants.SOUTH;
					break;
				case SWT.ARROW_UP:
					direction = PositionConstants.NORTH;
					break;
				case SWT.ARROW_RIGHT:
					direction = PositionConstants.EAST;
					break;
				case SWT.ARROW_LEFT:
					direction = PositionConstants.WEST;
					break;
			}

			boolean consumed = false;
			if (direction != 0 && event.stateMask == 0)
				consumed = navigateNextAnchor(direction);
			if (!consumed) {
				event.stateMask |= SWT.CONTROL;
				event.stateMask &= ~SWT.SHIFT;
				if (getCurrentViewer().getKeyHandler().keyPressed(event)) {
					navigateNextAnchor(0);
					updateTargetRequest();
					updateTargetUnderMouse();
					Command command = getCommand();
					if (command != null)
						setCurrentCommand(command);
					return true;
				}
			}
		}
	
		if (acceptConnectionStart(event)) {
			updateTargetUnderMouse();
			setConnectionSource(getTargetEditPart());
			((CreateConnectionRequest)getTargetRequest())
				.setSourceEditPart(getTargetEditPart());
			setState(STATE_ACCESSIBLE_DRAG_IN_PROGRESS);
			ourPlaceMouseInViewer(getLocation().getTranslated(6, 6));
			return true;
		}
	
		if (acceptConnectionFinish(event)) {
			Command command = getCommand();
			if (command != null && command.canExecute()) {
				setState(STATE_INITIAL);
				ourPlaceMouseInViewer(getLocation().getTranslated(6, 6));
				eraseSourceFeedback();
				eraseTargetFeedback();
				setCurrentCommand(command);
				executeCurrentCommand();
				handleFinished();
			}
			return true;
		}
	
		return super.handleKeyDown(event);
	}

	boolean navigateNextAnchor(int direction) {
		EditPart focus = getCurrentViewer().getFocusEditPart();
		AccessibleAnchorProvider provider;
		provider = (AccessibleAnchorProvider)focus.getAdapter(AccessibleAnchorProvider.class);
		if (provider == null)
			return false;

		List list;
		if (isInState(STATE_ACCESSIBLE_DRAG_IN_PROGRESS))
			list = provider.getTargetAnchorLocations();
		else
			list = provider.getSourceAnchorLocations();

		Point start = getLocation();
		int distance = Integer.MAX_VALUE;
		Point next = null;
		for (int i = 0; i < list.size(); i++) {
			Point p = (Point)list.get(i);
			if (p.equals(start)
				|| (direction != 0
					&& (start.getPosition(p) != direction)))
				continue;
			int d = p.getDistanceOrthogonal(start);
			if (d < distance) {
				distance = d;
				next = p;
			}
		}

		if (next != null) {
			ourPlaceMouseInViewer(next);
			return true;
		}
		return false;	
	}
	
	void ourPlaceMouseInViewer(Point p) {
		if (getCurrentViewer() == null)
			return;
		Control c = getCurrentViewer().getControl();
		Rectangle rect;
		if (c instanceof Scrollable)
			rect = ((Scrollable)c).getClientArea();
		else
			rect = c.getBounds();
		if (p.x > rect.x + rect.width - 1)
			p.x = rect.x + rect.width - 1;
		else if (p.x < rect.x)
			p.x = rect.x;
		if (p.y > rect.y + rect.height - 1)
			p.y = rect.y + rect.height - 1;
		else if (p.y < rect.y)
			p.y = rect.y;
		org.eclipse.swt.graphics.Point swt = new org.eclipse.swt.graphics.Point(p.x, p.y);
		swt = c.toDisplay(swt);
		c.getDisplay().setCursorLocation(swt);
	}
}
