/*******************************************************************************
 * Copyright (c) 2003, 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.gef;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.PrecisionRectangle;
import org.eclipse.draw2d.geometry.Rectangle;

import org.eclipse.gef.handles.HandleBounds;
import org.eclipse.gef.requests.GroupRequest;

/**
 * A temporary helper used to perform snapping to existing elements. This helper
 * can be used in conjunction with the
 * {@link org.eclipse.gef.tools.DragEditPartsTracker DragEditPartsTracker} when
 * dragging editparts within a graphical viewer. Snapping is based on the
 * existing children of a <I>container</I>. When snapping a rectangle, the edges
 * of the rectangle will snap to edges of other rectangles generated from the
 * children of the given container. Similarly, the centers and middles of
 * rectangles will snap to each other.
 * <P>
 * If the snap request is being made during a Move, Reparent or Resize, then the
 * figures of the participants of that request will not be used for snapping. If
 * the request is a Clone, then the figures for the parts being cloned will be
 * used as possible snap locations.
 * <P>
 * This helper does not keep up with changes made to the container editpart.
 * Clients should instantiate a new helper each time one is requested and not
 * hold on to instances of the helper.
 * 
 * @since 3.0
 * @author Randy Hudson
 * @author Pratik Shah
 */
public class SnapToGeometry extends SnapToHelper {

	/**
	 * A property indicating whether this helper should be used. The value
	 * should be an instance of Boolean. Currently, this class does not check to
	 * see if the viewer property is set to <code>true</code>.
	 * 
	 * @see EditPartViewer#setProperty(String, Object)
	 */
	public static final String PROPERTY_SNAP_ENABLED = "SnapToGeometry.isEnabled"; //$NON-NLS-1$

	/**
	 * The key used to identify the North anchor point in the extended data of a
	 * request. The north anchor may be set to an {@link Integer} value
	 * indicating where the snapping is occurring. This is used for feedback
	 * purposes.
	 */
	public static final String KEY_NORTH_ANCHOR = "SnapToGeometry.NorthAnchor"; //$NON-NLS-1$

	/**
	 * The key used to identify the South anchor point in the extended data of a
	 * request. The south anchor may be set to an {@link Integer} value
	 * indicating where the snapping is occurring. This is used for feedback
	 * purposes.
	 */
	public static final String KEY_SOUTH_ANCHOR = "SnapToGeometry.SouthAnchor"; //$NON-NLS-1$

	/**
	 * The key used to identify the West anchor point in the extended data of a
	 * request. The west anchor may be set to an {@link Integer} value
	 * indicating where the snapping is occurring. This is used for feedback
	 * purposes.
	 */
	public static final String KEY_WEST_ANCHOR = "SnapToGeometry.WestAnchor"; //$NON-NLS-1$

	/**
	 * The key used to identify the East anchor point in the extended data of a
	 * request. The east anchor may be set to an {@link Integer} value
	 * indicating where the snapping is occurring. This is used for feedback
	 * purposes.
	 */
	public static final String KEY_EAST_ANCHOR = "SnapToGeometry.EastAnchor"; //$NON-NLS-1$

	/**
	 * A vertical or horizontal snapping point. since 3.0
	 */
	protected static class Entry {
		final int type;
		final int location;

		/**
		 * Constructs a new entry of the given type and location.
		 * 
		 * @param type
		 *            an integer between -1 and 1 inclusively
		 * @param location
		 *            the location
		 */
		protected Entry(int type, int location) {
			if (type < -1 || type > 1)
				throw new IllegalArgumentException("Unrecognized snap type"); //$NON-NLS-1$
			this.type = type;
			this.location = location;
		}

		/**
		 * Returns the location of the snap entry.
		 * 
		 * @return the location
		 * @since 3.2
		 */
		public int getLocation() {
			return location;
		}

		/**
		 * Returns the snap type. The following values may be returned.
		 * <UL>
		 * <LI>-1 indicates left/top
		 * <LI>0 indicates middle/center
		 * <LI>1 indicates right/bottom
		 * </UL>
		 * 
		 * @return the snap type
		 * @since 3.2
		 */
		public int getType() {
			return type;
		}
	}

	/**
	 * The sensitivity of the snapping. Corrections greater than this value will
	 * not occur.
	 */
	protected static final double THRESHOLD = 5.0001;

	private double threshold = THRESHOLD;

	boolean cachedCloneBool;

	/**
	 * The horizontal rows being snapped to.
	 */
	protected Entry rows[];

	/**
	 * The vertical columnd being snapped to.
	 */
	protected Entry cols[];

	/**
	 * The container editpart providing the coordinates and the children to
	 * which snapping occurs.
	 */
	protected GraphicalEditPart container;

	/**
	 * Constructs a helper that will use the given part as its basis for
	 * snapping. The part's contents pane will provide the coordinate system and
	 * its children determine the existing elements.
	 * 
	 * @since 3.0
	 * @param container
	 *            the container editpart
	 */
	public SnapToGeometry(GraphicalEditPart container) {
		this.container = container;
	}

	/**
	 * Get the sensitivity of the snapping. Corrections greater than this value
	 * will not occur.
	 * 
	 * @return the snapping threshold
	 * @since 3.4
	 */
	protected double getThreshold() {
		return this.threshold;
	}

	/**
	 * Set the sensitivity of the snapping.
	 * 
	 * @see #getThreshold()
	 * @param newThreshold
	 *            the new snapping threshold
	 * @since 3.4
	 */
	protected void setThreshold(double newThreshold) {
		this.threshold = newThreshold;
	}

	/**
	 * Generates a list of parts which should be snapped to. The list is the
	 * original children, minus the given exclusions, minus and children whose
	 * figures are not visible.
	 * 
	 * @since 3.0
	 * @param exclusions
	 *            the children to exclude
	 * @return a list of parts which should be snapped to
	 */
	protected List generateSnapPartsList(List exclusions) {
		// Don't snap to any figure that is being dragged
		List children = new ArrayList(container.getChildren());
		children.removeAll(exclusions);

		// Don't snap to hidden figures
		List hiddenChildren = new ArrayList();
		for (Iterator iter = children.iterator(); iter.hasNext();) {
			GraphicalEditPart child = (GraphicalEditPart) iter.next();
			if (!child.getFigure().isVisible())
				hiddenChildren.add(child);
		}
		children.removeAll(hiddenChildren);

		return children;
	}

	/**
	 * Returns the correction value for the given entries and sides. During a
	 * move, the left, right, or center is free to snap to a location.
	 * 
	 * @param entries
	 *            the entries
	 * @param extendedData
	 *            the requests extended data
	 * @param vert
	 *            <code>true</code> if the correction is vertical
	 * @param near
	 *            the left/top side of the rectangle
	 * @param far
	 *            the right/bottom side of the rectangle
	 * @return the correction amount or #getThreshold () if no correction was
	 *         made
	 */
	protected double getCorrectionFor(Entry entries[], Map extendedData,
			boolean vert, double near, double far) {
		far -= 1.0;
		double total = near + far;
		// If the width is even (i.e., odd right now because we have reduced one
		// pixel from
		// far) there is no middle pixel so favor the left-most/top-most pixel
		// (which is what
		// populateRowsAndCols() does by using int precision).
		if ((int) (near - far) % 2 != 0)
			total -= 1.0;
		double result = getCorrectionFor(entries, extendedData, vert,
				total / 2, 0);
		if (result == getThreshold())
			result = getCorrectionFor(entries, extendedData, vert, near, -1);
		if (result == getThreshold())
			result = getCorrectionFor(entries, extendedData, vert, far, 1);
		return result;
	}

	/**
	 * Returns the correction value between ± {@link #getThreshold()}, or the
	 * #getThreshold () if no corrections were found.
	 * 
	 * @param entries
	 *            the entries
	 * @param extendedData
	 *            the map for setting values
	 * @param vert
	 *            <code>true</code> if vertical
	 * @param value
	 *            the value being corrected
	 * @param side
	 *            which sides should be considered
	 * @return the correction or #getThreshold () if no correction was made
	 */
	protected double getCorrectionFor(Entry entries[], Map extendedData,
			boolean vert, double value, int side) {
		double resultMag = getThreshold();
		double result = getThreshold();

		String property;
		if (side == -1)
			property = vert ? KEY_WEST_ANCHOR : KEY_NORTH_ANCHOR;
		else
			property = vert ? KEY_EAST_ANCHOR : KEY_SOUTH_ANCHOR;

		for (int i = 0; i < entries.length; i++) {
			Entry entry = entries[i];
			double magnitude;

			if (entry.type == -1 && side != 0) {
				magnitude = Math.abs(value - entry.location);
				if (magnitude < resultMag) {
					resultMag = magnitude;
					result = entry.location - value;
					extendedData.put(property, new Integer(entry.location));
				}
			} else if (entry.type == 0 && side == 0) {
				magnitude = Math.abs(value - entry.location);
				if (magnitude < resultMag) {
					resultMag = magnitude;
					result = entry.location - value;
					extendedData.put(property, new Integer(entry.location));
				}
			} else if (entry.type == 1 && side != 0) {
				magnitude = Math.abs(value - entry.location);
				if (magnitude < resultMag) {
					resultMag = magnitude;
					result = entry.location - value;
					extendedData.put(property, new Integer(entry.location));
				}
			}
		}
		return result;
	}

	/**
	 * Returns the rectangular contribution for the given editpart. This is the
	 * rectangle with which snapping is performed.
	 * 
	 * @since 3.0
	 * @param part
	 *            the child
	 * @return the rectangular guide for that part
	 */
	protected Rectangle getFigureBounds(GraphicalEditPart part) {
		IFigure fig = part.getFigure();
		if (fig instanceof HandleBounds)
			return ((HandleBounds) fig).getHandleBounds();
		return fig.getBounds();
	}

	/**
	 * Updates the cached row and column Entries using the provided parts.
	 * 
	 * @since 3.0
	 * @param parts
	 *            a List of EditParts
	 */
	protected void populateRowsAndCols(List parts) {
		rows = new Entry[parts.size() * 3];
		cols = new Entry[parts.size() * 3];
		for (int i = 0; i < parts.size(); i++) {
			GraphicalEditPart child = (GraphicalEditPart) parts.get(i);
			Rectangle bounds = getFigureBounds(child);
			cols[i * 3] = new Entry(-1, bounds.x);
			rows[i * 3] = new Entry(-1, bounds.y);
			cols[i * 3 + 1] = new Entry(0, bounds.x + (bounds.width - 1) / 2);
			rows[i * 3 + 1] = new Entry(0, bounds.y + (bounds.height - 1) / 2);
			cols[i * 3 + 2] = new Entry(1, bounds.right() - 1);
			rows[i * 3 + 2] = new Entry(1, bounds.bottom() - 1);
		}
	}

	/**
	 * @see SnapToHelper#snapRectangle(Request, int, PrecisionRectangle,
	 *      PrecisionRectangle)
	 */
	public int snapRectangle(Request request, int snapOrientation,
			PrecisionRectangle baseRect, PrecisionRectangle result) {

		baseRect = baseRect.getPreciseCopy();
		makeRelative(container.getContentPane(), baseRect);
		PrecisionRectangle correction = new PrecisionRectangle();
		makeRelative(container.getContentPane(), correction);

		// Recalculate snapping locations if needed
		boolean isClone = request.getType().equals(RequestConstants.REQ_CLONE);
		if (rows == null || cols == null || isClone != cachedCloneBool) {
			cachedCloneBool = isClone;
			List exclusionSet = Collections.EMPTY_LIST;
			if (!isClone && request instanceof GroupRequest)
				exclusionSet = ((GroupRequest) request).getEditParts();
			populateRowsAndCols(generateSnapPartsList(exclusionSet));
		}

		if ((snapOrientation & HORIZONTAL) != 0) {
			double xcorrect = getThreshold();
			xcorrect = getCorrectionFor(cols, request.getExtendedData(), true,
					baseRect.preciseX(), baseRect.preciseRight());
			if (xcorrect != getThreshold()) {
				snapOrientation &= ~HORIZONTAL;
				correction.setPreciseX(correction.preciseX() + xcorrect);
			}
		}

		if ((snapOrientation & VERTICAL) != 0) {
			double ycorrect = getThreshold();
			ycorrect = getCorrectionFor(rows, request.getExtendedData(), false,
					baseRect.preciseY(), baseRect.preciseBottom());
			if (ycorrect != getThreshold()) {
				snapOrientation &= ~VERTICAL;
				correction.setPreciseY(correction.preciseY() + ycorrect);
			}
		}

		if ((snapOrientation & EAST) != 0) {
			double rightCorrection = getCorrectionFor(cols,
					request.getExtendedData(), true,
					baseRect.preciseRight() - 1, 1);
			if (rightCorrection != getThreshold()) {
				snapOrientation &= ~EAST;
				correction.setPreciseWidth(correction.preciseWidth()
						+ rightCorrection);
			}
		}

		if ((snapOrientation & WEST) != 0) {
			double leftCorrection = getCorrectionFor(cols,
					request.getExtendedData(), true, baseRect.preciseX(), -1);
			if (leftCorrection != getThreshold()) {
				snapOrientation &= ~WEST;
				correction.setPreciseWidth(correction.preciseWidth()
						- leftCorrection);
				correction.setPreciseX(correction.preciseX() + leftCorrection);
			}
		}

		if ((snapOrientation & SOUTH) != 0) {
			double bottom = getCorrectionFor(rows, request.getExtendedData(),
					false, baseRect.preciseBottom() - 1, 1);
			if (bottom != getThreshold()) {
				snapOrientation &= ~SOUTH;
				correction
						.setPreciseHeight(correction.preciseHeight() + bottom);
			}
		}

		if ((snapOrientation & NORTH) != 0) {
			double topCorrection = getCorrectionFor(rows,
					request.getExtendedData(), false, baseRect.preciseY(), -1);
			if (topCorrection != getThreshold()) {
				snapOrientation &= ~NORTH;
				correction.setPreciseHeight(correction.preciseHeight()
						- topCorrection);
				correction.setPreciseY(correction.preciseY() + topCorrection);
			}
		}

		makeAbsolute(container.getContentPane(), correction);
		result.setPreciseX(result.preciseX() + correction.preciseX());
		result.setPreciseY(result.preciseY() + correction.preciseY());
		result.setPreciseWidth(result.preciseWidth()
				+ correction.preciseWidth());
		result.setPreciseHeight(result.preciseHeight()
				+ correction.preciseHeight());
		return snapOrientation;
	}

}
