/*******************************************************************************
 * Copyright (c) 2005, 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.Iterator;
import java.util.List;

import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.Rectangle;

/**
 * Clipping strategy for connection layer, which takes into account nested view
 * ports and truncates those parts of connections which reach outside and are
 * thus not visible.
 * 
 * @author Alexander Nyssen
 * @author Philip Ritzkopf
 * 
 * @since 3.6
 */
public class ViewportAwareConnectionLayerClippingStrategy implements
		IClippingStrategy {

	private static final Insets PRIVATE_INSETS = new Insets(0, 0, 1, 1);

	private ConnectionLayer connectionLayer = null;

	public ViewportAwareConnectionLayerClippingStrategy(
			ConnectionLayer connectionLayer) {
		this.connectionLayer = connectionLayer;
	}

	/**
	 * @see org.eclipse.draw2d.IClippingStrategy#getClip(org.eclipse.draw2d.IFigure)
	 */
	public Rectangle[] getClip(IFigure figure) {
		Rectangle[] clipRect = null;
		if (figure instanceof Connection) {
			clipRect = getEdgeClippingRectangle((Connection) figure);
		} else {
			clipRect = new Rectangle[] { getNodeClippingRectangle(figure) };
		}
		// translate clipping rectangles (which are in absolute coordinates)
		// to be relative to the parent figure's (i.e. the connection
		// layer's) client area
		for (int i = 0; i < clipRect.length; i++) {
			figure.translateToRelative(clipRect[i]);
		}
		return clipRect;
	}

	/**
	 * Computes clipping rectangle(s) for a given connection. Will consider all
	 * enclosing viewports, excluding the root viewport.
	 */
	protected Rectangle[] getEdgeClippingRectangle(Connection connection) {
		// start with clipping the connection at its original bounds
		Rectangle clipRect = getAbsoluteBoundsAsCopy(connection);

		// in case we cannot infer source and target of the connection (e.g.
		// if XYAnchors are used), returning the bounds is all we can do
		ConnectionAnchor sourceAnchor = connection.getSourceAnchor();
		ConnectionAnchor targetAnchor = connection.getTargetAnchor();
		if (sourceAnchor == null || sourceAnchor.getOwner() == null
				|| targetAnchor == null || targetAnchor.getOwner() == null) {
			return new Rectangle[] { clipRect };
		}

		// source and target figure are known, see if there is common
		// viewport
		// the connection has to be clipped at.
		IFigure sourceFigure = sourceAnchor.getOwner();
		IFigure targetFigure = targetAnchor.getOwner();
		Viewport nearestEnclosingCommonViewport = ViewportUtilities
				.getNearestCommonViewport(sourceFigure, targetFigure);
		if (nearestEnclosingCommonViewport == null) {
			return new Rectangle[] { clipRect };
		}

		// if the nearest common viewport is not the root viewport, we may
		// start with clipping the connection at this viewport.
		if (nearestEnclosingCommonViewport != getRootViewport()) {
			clipRect.intersect(getNodeClippingRectangle(nearestEnclosingCommonViewport));
		}

		// if the nearest common viewport of source and target is not
		// simultaneously
		// the nearest enclosing viewport of source and target respectively, the
		// connection has to be further clipped (the connection may even not be
		// visible at all)
		Viewport nearestEnclosingSourceViewport = ViewportUtilities
				.getNearestEnclosingViewport(sourceFigure);
		Viewport nearestEnclosingTargetViewport = ViewportUtilities
				.getNearestEnclosingViewport(targetFigure);
		if (nearestEnclosingSourceViewport != nearestEnclosingTargetViewport) {
			// compute if source and target anchor are visible
			// within the nearest common enclosing viewport (which may
			// itself be nested in other viewports).
			Rectangle sourceClipRect = getAbsoluteBoundsAsCopy(sourceFigure);
			if (nearestEnclosingSourceViewport != nearestEnclosingCommonViewport) {
				clipAtViewports(sourceClipRect,
						ViewportUtilities.getViewportsPath(
								nearestEnclosingSourceViewport,
								nearestEnclosingCommonViewport, false));
			}
			Rectangle targetClipRect = getAbsoluteBoundsAsCopy(targetFigure);
			if (nearestEnclosingTargetViewport != nearestEnclosingCommonViewport) {
				clipAtViewports(targetClipRect,
						ViewportUtilities.getViewportsPath(
								nearestEnclosingTargetViewport,
								nearestEnclosingCommonViewport, false));
			}
			PointList absolutePointsAsCopy = getAbsolutePointsAsCopy(connection);
			boolean sourceAnchorVisible = sourceClipRect.getExpanded(
					PRIVATE_INSETS).contains(
					absolutePointsAsCopy.getFirstPoint());
			boolean targetAnchorVisible = targetClipRect.getExpanded(
					PRIVATE_INSETS).contains(
					absolutePointsAsCopy.getLastPoint());

			if (!sourceAnchorVisible || !targetAnchorVisible) {
				// one (or both) of source or target anchor is invisible
				// within the nearest common viewport, so up to now
				// we regard the edge as invisible.
				return new Rectangle[] {};
				// TODO: We could come up with a more decent strategy here,
				// which also computes clipping fragments in those cases
				// where source/target are not visible but the edge
				// intersects with the enclosing source/target viewport's
				// parents bounds.

			} else {
				// both ends are visible, so just return what we have
				// computed before
				// (clipping at nearest enclosing viewport)
				return new Rectangle[] { clipRect };
			}
		} else {
			// source and target share the same enclosing viewport, so just
			// return what we have computed before (clipping at nearest
			// enclosing viewport)
			return new Rectangle[] { clipRect };
		}
	}

	/**
	 * Computes clipping rectangle for a given (node) figure. Will consider all
	 * enclosing viewports, excluding the root viewport.
	 */
	protected Rectangle getNodeClippingRectangle(IFigure figure) {
		// start with the bounds of the edit part's figure
		Rectangle clipRect = getAbsoluteBoundsAsCopy(figure);

		// now traverse the viewport path of the figure (and reduce clipRect
		// to what is actually visible); process all viewports up to the
		// root viewport
		List enclosingViewportsPath = ViewportUtilities.getViewportsPath(
				ViewportUtilities.getNearestEnclosingViewport(figure),
				getRootViewport(), false);
		clipAtViewports(clipRect, enclosingViewportsPath);
		return clipRect;
	}

	/**
	 * Clips the given clipRect at all given viewports.
	 */
	protected void clipAtViewports(Rectangle clipRect,
			List enclosingViewportsPath) {
		for (Iterator iterator = enclosingViewportsPath.iterator(); iterator
				.hasNext();) {
			Viewport viewport = (Viewport) iterator.next();
			clipRect.intersect(getAbsoluteViewportAreaAsCopy(viewport));
		}
	}

	/**
	 * Returns the root viewport, i.e. the nearest enclosing viewport of the
	 * connection layer, which corresponds to the nearest enclosing common
	 * viewport of primary and connection layer.
	 */
	protected Viewport getRootViewport() {
		return ViewportUtilities.getNearestEnclosingViewport(connectionLayer);
	}

	/**
	 * Returns the connection's points in absolute coordinates.
	 */
	protected PointList getAbsolutePointsAsCopy(Connection connection) {
		PointList points = connection.getPoints().getCopy();
		connection.translateToAbsolute(points);
		return points;
	}

	/**
	 * Returns the area covered by the viewport in absolute coordinates.
	 */
	protected Rectangle getAbsoluteViewportAreaAsCopy(Viewport viewport) {
		return getAbsoluteClientAreaAsCopy(viewport);
	}

	/**
	 * Returns the viewport's client area in absolute coordinates.
	 */
	protected Rectangle getAbsoluteClientAreaAsCopy(IFigure figure) {
		Rectangle absoluteClientArea = figure.getClientArea();
		figure.translateToParent(absoluteClientArea);
		figure.translateToAbsolute(absoluteClientArea);
		return absoluteClientArea;
	}

	/**
	 * Returns the figure's bounds in absolute coordinates.
	 */
	protected Rectangle getAbsoluteBoundsAsCopy(IFigure figure) {
		Rectangle absoluteFigureBounds = figure.getBounds().getCopy();
		figure.translateToAbsolute(absoluteFigureBounds);
		return absoluteFigureBounds;
	}
}