/*******************************************************************************
 * Copyright (c) 2000, 2010 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.draw2d;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.PrecisionPoint;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.draw2d.graph.Path;
import org.eclipse.draw2d.graph.ShortestPathRouter;

/**
 * Routes multiple connections around the children of a given container figure.
 * 
 * @author Whitney Sorenson
 * @author Randy Hudson
 * @since 3.1
 */
public final class ShortestPathConnectionRouter extends AbstractRouter {

	private class LayoutTracker extends LayoutListener.Stub {
		public void postLayout(IFigure container) {
			processLayout();
		}

		public void remove(IFigure child) {
			removeChild(child);
		}

		public void setConstraint(IFigure child, Object constraint) {
			addChild(child);
		}
	}

	private Map constraintMap = new HashMap();
	private Map figuresToBounds;
	private Map connectionToPaths;
	private boolean isDirty;
	private ShortestPathRouter algorithm = new ShortestPathRouter();
	private IFigure container;
	private Set staleConnections = new HashSet();
	private LayoutListener listener = new LayoutTracker();

	private FigureListener figureListener = new FigureListener() {
		public void figureMoved(IFigure source) {
			Rectangle newBounds = source.getBounds().getCopy();
			if (algorithm.updateObstacle(
					(Rectangle) figuresToBounds.get(source), newBounds)) {
				queueSomeRouting();
				isDirty = true;
			}

			figuresToBounds.put(source, newBounds);
		}
	};
	private boolean ignoreInvalidate;

	/**
	 * Creates a new shortest path router with the given container. The
	 * container contains all the figure's which will be treated as obstacles
	 * for the connections to avoid. Any time a child of the container moves,
	 * one or more connections will be revalidated to process the new obstacle
	 * locations. The connections being routed must not be contained within the
	 * container.
	 * 
	 * @param container
	 *            the container
	 */
	public ShortestPathConnectionRouter(IFigure container) {
		isDirty = false;
		algorithm = new ShortestPathRouter();
		this.container = container;
	}

	void addChild(IFigure child) {
		if (connectionToPaths == null)
			return;
		if (figuresToBounds.containsKey(child))
			return;
		Rectangle bounds = child.getBounds().getCopy();
		algorithm.addObstacle(bounds);
		figuresToBounds.put(child, bounds);
		child.addFigureListener(figureListener);
		isDirty = true;
	}

	private void hookAll() {
		figuresToBounds = new HashMap();
		for (int i = 0; i < container.getChildren().size(); i++)
			addChild((IFigure) container.getChildren().get(i));
		container.addLayoutListener(listener);
	}

	private void unhookAll() {
		container.removeLayoutListener(listener);
		if (figuresToBounds != null) {
			Iterator figureItr = figuresToBounds.keySet().iterator();
			while (figureItr.hasNext()) {
				// Must use iterator's remove to avoid concurrent modification
				IFigure child = (IFigure) figureItr.next();
				figureItr.remove();
				removeChild(child);
			}
			figuresToBounds = null;
		}
	}

	/**
	 * Gets the constraint for the given {@link Connection}. The constraint is
	 * the paths list of bend points for this connection.
	 * 
	 * @param connection
	 *            The connection whose constraint we are retrieving
	 * @return The constraint
	 */
	public Object getConstraint(Connection connection) {
		return constraintMap.get(connection);
	}

	/**
	 * Returns the default spacing maintained on either side of a connection.
	 * The default value is 4.
	 * 
	 * @return the connection spacing
	 * @since 3.2
	 */
	public int getSpacing() {
		return algorithm.getSpacing();
	}

	/**
	 * @see ConnectionRouter#invalidate(Connection)
	 */
	public void invalidate(Connection connection) {
		if (ignoreInvalidate)
			return;
		staleConnections.add(connection);
		isDirty = true;
	}

	private void processLayout() {
		if (staleConnections.isEmpty())
			return;
		((Connection) staleConnections.iterator().next()).revalidate();
	}

	private void processStaleConnections() {
		Iterator iter = staleConnections.iterator();
		if (iter.hasNext() && connectionToPaths == null) {
			connectionToPaths = new HashMap();
			hookAll();
		}

		while (iter.hasNext()) {
			Connection conn = (Connection) iter.next();

			Path path = (Path) connectionToPaths.get(conn);
			if (path == null) {
				path = new Path(conn);
				connectionToPaths.put(conn, path);
				algorithm.addPath(path);
			}

			List constraint = (List) getConstraint(conn);
			if (constraint == null)
				constraint = Collections.EMPTY_LIST;

			Point start = conn.getSourceAnchor().getReferencePoint().getCopy();
			Point end = conn.getTargetAnchor().getReferencePoint().getCopy();

			container.translateToRelative(start);
			container.translateToRelative(end);

			path.setStartPoint(start);
			path.setEndPoint(end);

			if (!constraint.isEmpty()) {
				PointList bends = new PointList(constraint.size());
				for (int i = 0; i < constraint.size(); i++) {
					Bendpoint bp = (Bendpoint) constraint.get(i);
					bends.addPoint(bp.getLocation());
				}
				path.setBendPoints(bends);
			} else
				path.setBendPoints(null);

			isDirty |= path.isDirty;
		}
		staleConnections.clear();
	}

	void queueSomeRouting() {
		if (connectionToPaths == null || connectionToPaths.isEmpty())
			return;
		try {
			ignoreInvalidate = true;
			((Connection) connectionToPaths.keySet().iterator().next())
					.revalidate();
		} finally {
			ignoreInvalidate = false;
		}
	}

	/**
	 * @see ConnectionRouter#remove(Connection)
	 */
	public void remove(Connection connection) {
		staleConnections.remove(connection);
		constraintMap.remove(connection);
		if (connectionToPaths == null)
			return;
		Path path = (Path) connectionToPaths.remove(connection);
		algorithm.removePath(path);
		isDirty = true;
		if (connectionToPaths.isEmpty()) {
			unhookAll();
			connectionToPaths = null;
		} else {
			// Make sure one of the remaining is revalidated so that we can
			// re-route again.
			queueSomeRouting();
		}
	}

	void removeChild(IFigure child) {
		if (connectionToPaths == null)
			return;
		Rectangle bounds = child.getBounds().getCopy();
		boolean change = algorithm.removeObstacle(bounds);
		figuresToBounds.remove(child);
		child.removeFigureListener(figureListener);
		if (change) {
			isDirty = true;
			queueSomeRouting();
		}
	}

	/**
	 * @see ConnectionRouter#route(Connection)
	 */
	public void route(Connection conn) {
		if (isDirty) {
			ignoreInvalidate = true;
			processStaleConnections();
			isDirty = false;
			List updated = algorithm.solve();
			Connection current;
			for (int i = 0; i < updated.size(); i++) {
				Path path = (Path) updated.get(i);
				current = (Connection) path.data;
				current.revalidate();

				PointList points = path.getPoints().getCopy();
				Point ref1, ref2, start, end;
				ref1 = new PrecisionPoint(points.getPoint(1));
				ref2 = new PrecisionPoint(points.getPoint(points.size() - 2));
				current.translateToAbsolute(ref1);
				current.translateToAbsolute(ref2);

				start = current.getSourceAnchor().getLocation(ref1).getCopy();
				end = current.getTargetAnchor().getLocation(ref2).getCopy();

				current.translateToRelative(start);
				current.translateToRelative(end);
				points.setPoint(start, 0);
				points.setPoint(end, points.size() - 1);

				current.setPoints(points);
			}
			ignoreInvalidate = false;
		}
	}

	/**
	 * @return All connection paths after routing dirty paths. Some of the paths
	 *         that were not dirty may change as well, as a consequence of new
	 *         routings.
	 * @since 3.5
	 */
	public List getPathsAfterRouting() {
		if (isDirty) {
			processStaleConnections();
			isDirty = false;
			List all = algorithm.solve();
			return all;

		}
		return null;
	}

	/**
	 * @see ConnectionRouter#setConstraint(Connection, Object)
	 */
	public void setConstraint(Connection connection, Object constraint) {
		// Connection.setConstraint() already calls revalidate, so we know that
		// a
		// route() call will follow.
		staleConnections.add(connection);
		constraintMap.put(connection, constraint);
		isDirty = true;
	}

	/**
	 * Sets the default space that should be maintained on either side of a
	 * connection. This causes the connections to be separated from each other
	 * and from the obstacles. The default value is 4.
	 * 
	 * @param spacing
	 *            the connection spacing
	 * @since 3.2
	 */
	public void setSpacing(int spacing) {
		algorithm.setSpacing(spacing);
	}

	/**
	 * @return true if there are connections routed by this router, false
	 *         otherwise
	 * @since 3.5
	 */
	public boolean hasMoreConnections() {
		return connectionToPaths != null && !connectionToPaths.isEmpty();
	}

	/**
	 * @return the container which contains connections routed by this router
	 * @since 3.5
	 */
	public IFigure getContainer() {
		return container;
	}

	/**
	 * Sets the value indicating if connection invalidation should be ignored.
	 * 
	 * @param b
	 *            true if invalidation should be skipped, false otherwise
	 * @since 3.5
	 */
	public void setIgnoreInvalidate(boolean b) {
		ignoreInvalidate = b;
	}

	/**
	 * Returns the value indicating if connection invalidation should be
	 * ignored.
	 * 
	 * @return true if invalidation should be skipped, false otherwise
	 * @since 3.5
	 */
	public boolean shouldIgnoreInvalidate() {
		return ignoreInvalidate;
	}

	/**
	 * Returns the value indicating if the router is dirty, i.e. if there are
	 * any outstanding connections that need to be routed
	 * 
	 * @return true if there are connections to be routed, false otherwise
	 * @since 3.5
	 */
	public boolean isDirty() {
		return isDirty;
	}

	/**
	 * Returns true if the given connection is routed by this router, false
	 * otherwise
	 * 
	 * @param conn
	 *            Connection whose router is questioned
	 * @return true if this is the router used for conn
	 * @since 3.5
	 */
	public boolean containsConnection(Connection conn) {
		return connectionToPaths != null && connectionToPaths.containsKey(conn);
	}

}
