/*******************************************************************************
 * 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;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;

import org.eclipse.bpel.ui.editparts.CollapsableEditPart;
import org.eclipse.bpel.ui.editparts.ProcessEditPart;
import org.eclipse.draw2d.FigureCanvas;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.PositionConstants;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.gef.ConnectionEditPart;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.GraphicalEditPart;
import org.eclipse.gef.GraphicalViewer;
import org.eclipse.gef.KeyHandler;
import org.eclipse.gef.requests.DirectEditRequest;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;

public class BPELGraphicalKeyHandler extends KeyHandler {
	int counter;

	/**
	 * When navigating through connections, a "Node" EditPart is used as a
	 * reference.
	 */
	private WeakReference<GraphicalEditPart> cachedNode;
	private GraphicalViewer viewer;

	public BPELGraphicalKeyHandler(GraphicalViewer viewer) {
		this.viewer = viewer;
	}

	private boolean acceptConnection(KeyEvent event) {
		return event.character == '/'
			|| event.character == '?'
			|| event.character == '\\'
			|| event.character == '\u001c'
			|| event.character == '|';
	}

	private boolean acceptIntoContainer(KeyEvent event) {
		return ((event.stateMask & SWT.ALT) != 0)
			&& (event.keyCode == SWT.ARROW_DOWN);
	}

	private boolean acceptLeaveConnection(KeyEvent event) {
		int key = event.keyCode;
		if (getFocus() instanceof ConnectionEditPart)
			if ((key == SWT.ARROW_UP)
				|| (key == SWT.ARROW_RIGHT)
				|| (key == SWT.ARROW_DOWN)
				|| (key == SWT.ARROW_LEFT))
				return true;
		return false;
	}

	private boolean acceptLeaveContents(KeyEvent event) {
		int key = event.keyCode;
		return getFocus() == getViewer().getContents()
			&& ((key == SWT.ARROW_UP)
				|| (key == SWT.ARROW_RIGHT)
				|| (key == SWT.ARROW_DOWN)
				|| (key == SWT.ARROW_LEFT));
	}

	private boolean acceptOutOf(KeyEvent event) {
		return ((event.stateMask & SWT.ALT) != 0)
			&& (event.keyCode == SWT.ARROW_UP);
	}

	private ConnectionEditPart findConnection(
		GraphicalEditPart node,
		ConnectionEditPart current,
		boolean forward) {
		List<ConnectionEditPart> connections = new ArrayList<ConnectionEditPart>(node.getSourceConnections());
		connections.addAll(node.getTargetConnections());
		if (connections.isEmpty())
			return null;
		if (forward)
			counter++;
		else
			counter--;
		while (counter < 0)
			counter += connections.size();
		counter %= connections.size();
		return connections.get(
			counter % connections.size());
	}

	/*
	 * pStart is a point in absolute coordinates.
	 */
	private GraphicalEditPart findSibling(
		List<GraphicalEditPart> siblings,
		Point pStart,
		int direction,
		EditPart exclude) {
		GraphicalEditPart epCurrent;
		GraphicalEditPart epFinal = null;
		IFigure figure;
		Point pCurrent;
		int distance = Integer.MAX_VALUE;

		Iterator<GraphicalEditPart> iter = siblings.iterator();
		while (iter.hasNext()) {
			epCurrent = iter.next();
			if (epCurrent == exclude || !epCurrent.isSelectable())
				continue;
			figure = epCurrent.getFigure();
			pCurrent = getInterestingPoint(figure);
			figure.translateToAbsolute(pCurrent);
			if (pStart.getPosition(pCurrent) != direction)
				continue;

			int d = pCurrent.getDistanceOrthogonal(pStart);
			if (d < distance) {
				distance = d;
				epFinal = epCurrent;
			}
		}
		return epFinal;
	}

	Point getInterestingPoint(IFigure figure) {
		return figure.getBounds().getCenter();
	}

	/**
	 * Returns the cached node. It is possible that the node is not longer in
	 * the viewer but has not been garbage collected yet.
	 */
	private GraphicalEditPart getCachedNode() {
		if (cachedNode == null)
			return null;
		if (cachedNode.isEnqueued())
			return null;
		return cachedNode.get();
	}

	protected GraphicalEditPart getFocus() {
		return (GraphicalEditPart) getViewer().getFocusEditPart();
	}

	/**
	 * Returns the viewer on which this key handler was created.
	 * 
	 * @return the viewer
	 */
	protected GraphicalViewer getViewer() {
		return viewer;
	}

	/**
	 * Extended to process key events described above.
	 * 
	 * @see org.eclipse.gef.KeyHandler#keyPressed(org.eclipse.swt.events.KeyEvent)
	 */
	@Override
	public boolean keyPressed(KeyEvent event) {
		if (event.character == ' ') {
			processSelect(event);
			return true;
		} else if (acceptIntoContainer(event)) {
			navigateIntoContainer(event);
			return true;
		} else if (acceptOutOf(event)) {
			navigateOut(event);
			return true;
		} else if (acceptConnection(event)) {
			navigateConnections(event);
			return true;
		} else if (acceptScroll(event)) {
			scrollViewer(event);
			return true;
		} else if (acceptLeaveConnection(event)) {
			navigateOutOfConnection(event);
			return true;
		} else if (acceptLeaveContents(event)) {
			navigateIntoContainer(event);
			return true;
		} else if (event.character == '[') {
			int found = -1;
			List<GraphicalEditPart> list = getProcessNavigationList();
			for (int i = 0; i < list.size(); i++) {
				if (getFocus() == list.get(i)) {
					found = i;
					break;
				}
			}
			if (found >= 0) {
				found--;
			}
			if (found >=0) {
				EditPart navigateTo = list.get(found);
				navigateTo(navigateTo, event);
			}
		} else if (event.character == ']') {
			int found = -1;
			List<GraphicalEditPart> list = getProcessNavigationList();
			for (int i = 0; i < list.size(); i++) {
				if (getFocus() == list.get(i)) {
					found = i;
					break;
				}
			}
			if (found >= 0) {
				found++;
			}
			if (found < list.size()) {
				EditPart navigateTo = list.get(found);
				navigateTo(navigateTo, event);
			}
		} else if (event.character == '+' || event.character == '=') {
			expand(event);
		} else if (event.character == '-') {
			collapse(event);
		}

	

		switch (event.keyCode) {
			case SWT.ARROW_LEFT :
				if (navigateNextSibling(event, PositionConstants.WEST))
					return true;
				break;
			case SWT.ARROW_RIGHT :
				if (navigateNextSibling(event, PositionConstants.EAST))
						return true;
				break;
			case SWT.ARROW_UP :
				if (navigateNextSibling(event, PositionConstants.NORTH))
					return true;
				break;
			case SWT.ARROW_DOWN :
				if (navigateNextSibling(event, PositionConstants.SOUTH))
					return true;
				break;

			case SWT.HOME :
				if (navigateJumpSibling(event, PositionConstants.WEST))
					return true;
				break;
			case SWT.END :
				if (navigateJumpSibling(event, PositionConstants.EAST))
					return true;
				break;
			case SWT.PAGE_DOWN :
				if (navigateJumpSibling(event, PositionConstants.SOUTH))
					return true;
				break;
			case SWT.PAGE_UP :
				if (navigateJumpSibling(event, PositionConstants.NORTH))
					return true;
				break;
			case SWT.TAB :
				return true;
			case SWT.F2:
				activateDirectEdit(event);
				break;
		}
		return super.keyPressed(event);
	}

	private void navigateConnections(KeyEvent event) {
		GraphicalEditPart focus = getFocus();
		ConnectionEditPart current = null;
		GraphicalEditPart node = getCachedNode();
		if (focus instanceof ConnectionEditPart) {
			current = (ConnectionEditPart) focus;
			if (node == null
				|| (node != current.getSource() && node != current.getTarget())) {
				node = (GraphicalEditPart) current.getSource();
				counter = 0;
			}
		} else {
			node = focus;
		}

		setCachedNode(node);
		boolean forward = event.character == '/' || event.character == '?';
		ConnectionEditPart next = findConnection(node, current, forward);
		navigateTo(next, event);
	}

	private void navigateIntoContainer(KeyEvent event) {
		GraphicalEditPart focus = getFocus();
		List<GraphicalEditPart> childList = getNavigationChildren(focus);
		Point tl = focus.getContentPane().getBounds().getTopLeft();

		int minimum = Integer.MAX_VALUE;
		int current;
		GraphicalEditPart closestPart = null;

		for (int i = 0; i < childList.size(); i++) {
			GraphicalEditPart ged = childList.get(i);
			if (!ged.isSelectable())
				continue;
			Rectangle childBounds = ged.getFigure().getBounds();

			current = (childBounds.x - tl.x) + (childBounds.y - tl.y);
			if (current < minimum) {
				minimum = current;
				closestPart = ged;
			}
		}
		if (closestPart != null)
			navigateTo(closestPart, event);
	}

	private boolean navigateJumpSibling(KeyEvent event, int direction) {
		// TODO: Implement navigateJumpSibling() (for PGUP, PGDN, HOME and END
		// key events)
		return false;
	}

	private boolean navigateNextSibling(KeyEvent event, int direction) {
		return navigateNextSibling(event, direction, getNavigationSiblings());
	}

	boolean navigateNextSibling(KeyEvent event, int direction, List<GraphicalEditPart> list) {
		GraphicalEditPart epStart = getFocus();
		IFigure figure = epStart.getFigure();
		Point pStart = getInterestingPoint(figure);
		figure.translateToAbsolute(pStart);
		EditPart next = findSibling(list, pStart, direction, epStart);
		if (next == null)
			return false;
		navigateTo(next, event);
		return true;
	}

	private void navigateOut(KeyEvent event) {
		EditPart parent = getNavigationParent(getFocus());
		if (getFocus() == null || getFocus() == getViewer().getContents() || getFocus().getParent() == getViewer().getContents())
			return;
		navigateTo(parent, event);
	}

	private void navigateOutOfConnection(KeyEvent event) {
		GraphicalEditPart cached = getCachedNode();
		ConnectionEditPart conn = (ConnectionEditPart) getFocus();
		if (cached != null && (cached == conn.getSource() || cached == conn.getTarget()))
			navigateTo(cached, event);
		else
			navigateTo(conn.getSource(), event);
	}

	void navigateTo(EditPart part, KeyEvent event) {
		if (part == null)
			return;
		if ((event.stateMask & SWT.SHIFT) != 0) {
			getViewer().appendSelection(part);
			getViewer().setFocus(part);
		} else if ((event.stateMask & SWT.CONTROL) != 0)
			getViewer().setFocus(part);
		else
			getViewer().select(part);
		getViewer().reveal(part);
	}

	private void processSelect(KeyEvent event) {
		EditPart part = getViewer().getFocusEditPart();
		if ((event.stateMask & SWT.CONTROL) != 0
			&& part.getSelected() != EditPart.SELECTED_NONE)
			getViewer().deselect(part);
		else
			getViewer().appendSelection(part);

		getViewer().setFocus(part);
	}

	private void expand(KeyEvent event) {
		EditPart part = getViewer().getFocusEditPart();
		if (part instanceof CollapsableEditPart) {
			((CollapsableEditPart)part).setCollapsed(false);
		}
	}

	private void collapse(KeyEvent event) {
		EditPart part = getViewer().getFocusEditPart();
		if (part instanceof CollapsableEditPart) {
			((CollapsableEditPart)part).setCollapsed(true);
		}
	}

	private void setCachedNode(GraphicalEditPart node) {
		if (node == null)
			cachedNode = null;
		else
			cachedNode = new WeakReference<GraphicalEditPart>(node);
	}

	/**
	 * handles StartNodeEditPart specific behavior
	 */
	protected List<GraphicalEditPart> getNavigationChildren(EditPart part) {
//		if (part instanceof StartNodeEditPart) {
//			// return the process's children
//			ProcessEditPart proc = (ProcessEditPart) part.getParent();
//			List list = proc.getChildren();
//			list.remove(part);
//			return list;
//		}
//		else
		return part.getChildren();
	}

	protected List<GraphicalEditPart> getNavigationSiblings() {
		EditPart focus = getFocus().getParent();
		return focus.getChildren();
	}
	
	protected EditPart getNavigationParent(EditPart part) {
		EditPart parent = part.getParent();
		if (parent instanceof ProcessEditPart) {
			return null;
		}
		return parent;
	}
	
	protected void addChildren(EditPart part, List<GraphicalEditPart> list) {
		List<GraphicalEditPart> children = part.getChildren();
		for (int i = 0; i < children.size(); i++) {
			list.add(children.get(i));
			addChildren((children.get(i)), list);
		}
		return;
	}
	/* 
	 * as a usability enhancement, we want to navigate to the next activity using the SHIFT left and right arrow, this
	 * allows the user to navigate to every element on the canvas easily
	 */ 
	protected List<GraphicalEditPart> getProcessNavigationList() {
		ProcessEditPart process = (ProcessEditPart)viewer.getContents();
		Vector<GraphicalEditPart> all = new Vector<GraphicalEditPart>();
		addChildren(process,all);
		return all;
	}
	
	protected boolean activateDirectEdit(KeyEvent event) {
		GraphicalEditPart currentPart = (GraphicalEditPart)viewer.getFocusEditPart();
		currentPart.performRequest(new DirectEditRequest());
		return true;
	}

	boolean acceptScroll(KeyEvent event) {
		return ((event.stateMask & SWT.CTRL) != 0 && (event.stateMask & SWT.SHIFT) != 0 
				&& (event.keyCode == SWT.ARROW_DOWN || event.keyCode == SWT.ARROW_LEFT
				|| event.keyCode == SWT.ARROW_RIGHT || event.keyCode == SWT.ARROW_UP));
	}

	void scrollViewer(KeyEvent event) {
		if (!(getViewer().getControl() instanceof FigureCanvas))
			return;
		FigureCanvas figCanvas = (FigureCanvas)getViewer().getControl();
		Point loc = figCanvas.getViewport().getViewLocation();
		Rectangle area = figCanvas.getViewport().getClientArea(Rectangle.SINGLETON).scale(.1); 
		switch (event.keyCode) {
			case SWT.ARROW_DOWN:
				figCanvas.scrollToY(loc.y + area.height);
				break;
			case SWT.ARROW_UP:
				figCanvas.scrollToY(loc.y - area.height);
				break;
			case SWT.ARROW_LEFT:
				figCanvas.scrollToX(loc.x - area.width);
				break;
			case SWT.ARROW_RIGHT:
				figCanvas.scrollToX(loc.x + area.width);
		}
	}
}
