/**
 * Copyright (c) 2009-2012, JGraph Ltd
 */
package com.mxgraph.util;

import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

public class mxCurve
{
	/**
	 * A collection of arrays of curve points
	 */
	protected Map<String, mxPoint[]> points;

	// Rectangle just completely enclosing branch and label/
	protected double minXBounds = 10000000;

	protected double maxXBounds = 0;

	protected double minYBounds = 10000000;

	protected double maxYBounds = 0;

	/**
	 * An array of arrays of intervals. These intervals define the distance
	 * along the edge (0 to 1) that each point lies
	 */
	protected Map<String, double[]> intervals;

	/**
	 * The curve lengths of the curves
	 */
	protected Map<String, Double> curveLengths;

	/**
	 * Defines the key for the central curve index
	 */
	public static String CORE_CURVE = "Center_curve";

	/**
	 * Defines the key for the label curve index
	 */
	public static String LABEL_CURVE = "Label_curve";;

	/**
	 * Indicates that an invalid position on a curve was requested
	 */
	public static mxLine INVALID_POSITION = new mxLine(new mxPoint(0, 0),
			new mxPoint(1, 0));

	/**
	 * Offset of the label curve from the curve the label curve is based on.
	 * If you wish to set this value, do so directly after creation of the curve.
	 * The first time the curve is used the label curve will be created with 
	 * whatever value is contained in this variable. Changes to it after that point 
	 * will have no effect.
	 */
	protected double labelBuffer = mxConstants.DEFAULT_LABEL_BUFFER;

	/**
	 * The points this curve is drawn through. These are typically control
	 * points and are at distances from each other that straight lines
	 * between them do not describe a smooth curve. This class takes
	 * these guiding points and creates a finer set of internal points
	 * that visually appears to be a curve when linked by straight lines
	 */
	public List<mxPoint> guidePoints = new ArrayList<mxPoint>();

	/**
	 * Whether or not the curve currently holds valid values
	 */
	protected boolean valid = false;

	/**
	 * 
	 */
	public void setLabelBuffer(double buffer)
	{
		labelBuffer = buffer;
	}

	/**
	 * 
	 */
	public mxRectangle getBounds()
	{
		if (!valid)
		{
			createCoreCurve();
		}
		return new mxRectangle(minXBounds, minYBounds, maxXBounds - minXBounds,
				maxYBounds - minYBounds);
	}

	/**
	 * 
	 */
	public mxCurve()
	{
	}

	/**
	 * 
	 */
	public mxCurve(List<mxPoint> points)
	{
		boolean nullPoints = false;

		for (mxPoint point : points)
		{
			if (point == null)
			{
				nullPoints = true;
				break;
			}
		}

		if (!nullPoints)
		{
			guidePoints = new ArrayList<mxPoint>(points);
		}
	}

	/**
	 * Calculates the index of the lower point on the segment
	 * that contains the point <i>distance</i> along the 
	 */
	protected int getLowerIndexOfSegment(String index, double distance)
	{
		double[] curveIntervals = getIntervals(index);

		if (curveIntervals == null)
		{
			return 0;
		}

		int numIntervals = curveIntervals.length;

		if (distance <= 0.0 || numIntervals < 3)
		{
			return 0;
		}

		if (distance >= 1.0)
		{
			return numIntervals - 2;
		}

		// Pick a starting index roughly where you expect the point
		// to be
		int testIndex = (int) (numIntervals * distance);

		if (testIndex >= numIntervals)
		{
			testIndex = numIntervals - 1;
		}

		// The max and min indices tested so far
		int lowerLimit = -1;
		int upperLimit = numIntervals;

		// It cannot take more than the number of intervals to find
		// the correct segment
		for (int i = 0; i < numIntervals; i++)
		{
			double segmentDistance = curveIntervals[testIndex];
			double multiplier = 0.5;

			if (distance < segmentDistance)
			{
				upperLimit = Math.min(upperLimit, testIndex);
				multiplier = -0.5;
			}
			else if (distance > segmentDistance)
			{
				lowerLimit = Math.max(lowerLimit, testIndex);
			}
			else
			{
				// Values equal
				if (testIndex == 0)
				{
					lowerLimit = 0;
					upperLimit = 1;
				}
				else
				{
					lowerLimit = testIndex - 1;
					upperLimit = testIndex;
				}
			}

			int indexDifference = upperLimit - lowerLimit;

			if (indexDifference == 1)
			{
				break;
			}

			testIndex = (int) (testIndex + indexDifference * multiplier);

			if (testIndex == lowerLimit)
			{
				testIndex = lowerLimit + 1;
			}

			if (testIndex == upperLimit)
			{
				testIndex = upperLimit - 1;
			}
		}

		if (lowerLimit != upperLimit - 1)
		{
			return -1;
		}

		return lowerLimit;
	}

	/**
	 * Returns a unit vector parallel to the curve at the specified
	 * distance along the curve. To obtain the angle the vector makes
	 * with (1,0) perform Math.atan(segVectorY/segVectorX).
	 * @param index the curve index specifying the curve to analyse
	 * @param distance the distance from start to end of curve (0.0...1.0)
	 * @return a unit vector at the specified point on the curve represented
	 * 		as a line, parallel with the curve. If the distance or curve is
	 * 		invalid, <code>mxCurve.INVALID_POSITION</code> is returned
	 */
	public mxLine getCurveParallel(String index, double distance)
	{
		mxPoint[] pointsCurve = getCurvePoints(index);
		double[] curveIntervals = getIntervals(index);

		if (pointsCurve != null && pointsCurve.length > 0
				&& curveIntervals != null && distance >= 0.0 && distance <= 1.0)
		{
			// If the curve is zero length, it will only have one point
			// We can't calculate in this case
			if (pointsCurve.length == 1)
			{
				mxPoint point = pointsCurve[0];
				return new mxLine(point.getX(), point.getY(), new mxPoint(1, 0));
			}

			int lowerLimit = getLowerIndexOfSegment(index, distance);
			mxPoint firstPointOfSeg = pointsCurve[lowerLimit];
			double segVectorX = pointsCurve[lowerLimit + 1].getX()
					- firstPointOfSeg.getX();
			double segVectorY = pointsCurve[lowerLimit + 1].getY()
					- firstPointOfSeg.getY();
			double distanceAlongSeg = (distance - curveIntervals[lowerLimit])
					/ (curveIntervals[lowerLimit + 1] - curveIntervals[lowerLimit]);
			double segLength = Math.sqrt(segVectorX * segVectorX + segVectorY
					* segVectorY);
			double startPointX = firstPointOfSeg.getX() + segVectorX
					* distanceAlongSeg;
			double startPointY = firstPointOfSeg.getY() + segVectorY
					* distanceAlongSeg;
			mxPoint endPoint = new mxPoint(segVectorX / segLength, segVectorY
					/ segLength);
			return new mxLine(startPointX, startPointY, endPoint);
		}
		else
		{
			return INVALID_POSITION;
		}
	}

	/**
	 * Returns a section of the curve as an array of points
	 * @param index the curve index specifying the curve to analyse
	 * @param start the start position of the curve segment (0.0...1.0)
	 * @param end the end position of the curve segment (0.0...1.0)
	 * @return a sequence of point representing the curve section or null
	 * 			if it cannot be calculated
	 */
	public mxPoint[] getCurveSection(String index, double start, double end)
	{
		mxPoint[] pointsCurve = getCurvePoints(index);
		double[] curveIntervals = getIntervals(index);

		if (pointsCurve != null && pointsCurve.length > 0
				&& curveIntervals != null && start >= 0.0 && start <= 1.0
				&& end >= 0.0 && end <= 1.0)
		{
			// If the curve is zero length, it will only have one point
			// We can't calculate in this case
			if (pointsCurve.length == 1)
			{
				mxPoint point = pointsCurve[0];
				return new mxPoint[] { new mxPoint(point.getX(), point.getY()) };
			}

			int lowerLimit = getLowerIndexOfSegment(index, start);
			mxPoint firstPointOfSeg = pointsCurve[lowerLimit];
			double segVectorX = pointsCurve[lowerLimit + 1].getX()
					- firstPointOfSeg.getX();
			double segVectorY = pointsCurve[lowerLimit + 1].getY()
					- firstPointOfSeg.getY();
			double distanceAlongSeg = (start - curveIntervals[lowerLimit])
					/ (curveIntervals[lowerLimit + 1] - curveIntervals[lowerLimit]);
			mxPoint startPoint = new mxPoint(firstPointOfSeg.getX()
					+ segVectorX * distanceAlongSeg, firstPointOfSeg.getY()
					+ segVectorY * distanceAlongSeg);

			List<mxPoint> result = new ArrayList<mxPoint>();
			result.add(startPoint);

			double current = start;
			current = curveIntervals[++lowerLimit];

			while (current <= end)
			{
				mxPoint nextPointOfSeg = pointsCurve[lowerLimit];
				result.add(nextPointOfSeg);
				current = curveIntervals[++lowerLimit];
			}

			// Add whatever proportion of the last segment has to 
			// be added to make the exactly end distance
			if (lowerLimit > 0 && lowerLimit < pointsCurve.length
					&& end > curveIntervals[lowerLimit - 1])
			{
				firstPointOfSeg = pointsCurve[lowerLimit - 1];
				segVectorX = pointsCurve[lowerLimit].getX()
						- firstPointOfSeg.getX();
				segVectorY = pointsCurve[lowerLimit].getY()
						- firstPointOfSeg.getY();
				distanceAlongSeg = (end - curveIntervals[lowerLimit - 1])
						/ (curveIntervals[lowerLimit] - curveIntervals[lowerLimit - 1]);
				mxPoint endPoint = new mxPoint(firstPointOfSeg.getX()
						+ segVectorX * distanceAlongSeg, firstPointOfSeg.getY()
						+ segVectorY * distanceAlongSeg);
				result.add(endPoint);
			}

			mxPoint[] resultArray = new mxPoint[result.size()];
			return result.toArray(resultArray);
		}
		else
		{
			return null;
		}
	}

	/**
	 * Returns whether or not the rectangle passed in hits any part of this
	 * curve.
	 * @param rect the rectangle to detect for a hit
	 * @return whether or not the rectangle hits this curve
	 */
	public boolean intersectsRect(Rectangle rect)
	{
		// To save CPU, we can test if the rectangle intersects the entire
		// bounds of this curve
		if (!getBounds().getRectangle().intersects(rect))
		{
			return false;
		}

		mxPoint[] pointsCurve = getCurvePoints(mxCurve.CORE_CURVE);

		if (pointsCurve != null && pointsCurve.length > 1)
		{
			mxRectangle mxRect = new mxRectangle(rect);
			// First check for any of the curve points lying within the 
			// rectangle, then for any of the curve segments intersecting 
			// with the rectangle sides
			for (int i = 1; i < pointsCurve.length; i++)
			{
				if (mxRect.contains(pointsCurve[i].getX(),
						pointsCurve[i].getY())
						|| mxRect.contains(pointsCurve[i - 1].getX(),
								pointsCurve[i - 1].getY()))
				{
					return true;
				}
			}

			for (int i = 1; i < pointsCurve.length; i++)
			{
				if (mxRect.intersectLine(pointsCurve[i].getX(),
						pointsCurve[i].getY(), pointsCurve[i - 1].getX(),
						pointsCurve[i - 1].getY()) != null)
				{
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Returns the point at which this curve intersects the boundary of 
	 * the given rectangle, if it does so. If it does not intersect, 
	 * null is returned. If it intersects multiple times, the first 
	 * intersection from the start end of the curve is returned.
	 * 
	 * @param index the curve index specifying the curve to analyse
	 * @param rect the whose boundary is to be tested for intersection
	 * with this curve
	 * @return the point at which this curve intersects the boundary of 
	 * the given rectangle, if it does so. If it does not intersect, 
	 * null is returned.
	 */
	public mxPoint intersectsRectPerimeter(String index, mxRectangle rect)
	{
		mxPoint result = null;
		mxPoint[] pointsCurve = getCurvePoints(index);

		if (pointsCurve != null && pointsCurve.length > 1)
		{
			int crossingSeg = intersectRectPerimeterSeg(index, rect);

			if (crossingSeg != -1)
			{
				result = intersectRectPerimeterPoint(index, rect, crossingSeg);
			}
		}

		return result;
	}

	/**
	 * Returns the distance from the start of the curve at which this 
	 * curve intersects the boundary of the given rectangle, if it does 
	 * so. If it does not intersect, -1 is returned. 
	 * If it intersects multiple times, the first intersection from 
	 * the start end of the curve is returned.
	 * 
	 * @param index the curve index specifying the curve to analyse
	 * @param rect the whose boundary is to be tested for intersection
	 * with this curve
	 * @return the distance along the curve from the start at which
	 * the intersection occurs
	 */
	public double intersectsRectPerimeterDist(String index, mxRectangle rect)
	{
		double result = -1;
		mxPoint[] pointsCurve = getCurvePoints(index);
		double[] curveIntervals = getIntervals(index);

		if (pointsCurve != null && pointsCurve.length > 1)
		{
			int segIndex = intersectRectPerimeterSeg(index, rect);
			mxPoint intersectPoint = null;

			if (segIndex != -1)
			{
				intersectPoint = intersectRectPerimeterPoint(index, rect,
						segIndex);
			}

			if (intersectPoint != null && curveIntervals != null)
			{
				double startSegX = pointsCurve[segIndex - 1].getX();
				double startSegY = pointsCurve[segIndex - 1].getY();
				double distToStartSeg = curveIntervals[segIndex - 1]
						* getCurveLength(index);
				double intersectOffsetX = intersectPoint.getX() - startSegX;
				double intersectOffsetY = intersectPoint.getY() - startSegY;
				double lenToIntersect = Math.sqrt(intersectOffsetX
						* intersectOffsetX + intersectOffsetY
						* intersectOffsetY);
				result = distToStartSeg + lenToIntersect;
			}
		}

		return result;
	}

	/**
	 * Returns a point to move the input rectangle to, in order to
	 * attempt to place the rectangle away from the curve. NOTE: Curves
	 * are scaled, the input rectangle should be also.
	 * @param index  the curve index specifying the curve to analyse
	 * @param rect the rectangle that is to be moved
	 * @param buffer the amount by which the rectangle is to be moved,
	 * 			beyond the dimensions of the rect
	 * @return the point to move the top left of the input rect to
	 * 			, otherwise null if no point can be determined
	 */
	public mxPoint collisionMove(String index, mxRectangle rect, double buffer)
	{
		int hitSeg = intersectRectPerimeterSeg(index, rect);

		// Could test for a second hit (the rect exit, unless the same 
		// segment is entry and exit) and allow for that in movement.
		
		if (hitSeg == -1)
		{
			return null;
		}
		else
		{
			mxPoint[] pointsCurve = getCurvePoints(index);
			if (pointsCurve == null)
			{
				return null;
			}

			double x0 = pointsCurve[hitSeg - 1].getX();
			double y0 = pointsCurve[hitSeg - 1].getY();
			double x1 = pointsCurve[hitSeg].getX();
			double y1 = pointsCurve[hitSeg].getY();
			
			double x = rect.getX();
			double y = rect.getY();
			double width = rect.getWidth();
			double height = rect.getHeight();

			// Whether the intersection is one of the horizontal sides of the rect
			@SuppressWarnings("unused")
			boolean horizIncident = false;
			mxPoint hitPoint = mxUtils.intersection(x, y, x + width, y, x0, y0, x1, y1);
			
			if (hitPoint != null)
			{
				horizIncident = true;
			}
			else
			{
				hitPoint = mxUtils.intersection(x + width, y, x + width, y + height,
						x0, y0, x1, y1);
			}

			if (hitPoint == null)
			{
				hitPoint = mxUtils.intersection(x + width, y + height, x, y + height,
						x0, y0, x1, y1);
				
				if (hitPoint != null)
				{
					horizIncident = true;
				}
				else
				{
					hitPoint = mxUtils.intersection(x, y, x, y + height, x0, y0, x1, y1);
				}
			}

			if (hitPoint != null)
			{
				
			}

		}
		
		return null;
	}

	/**
	 * Utility method to determine within which segment the specified rectangle
	 * intersects the specified curve
	 * 
	 * @param index the curve index specifying the curve to analyse
	 * @param rect the whose boundary is to be tested for intersection
	 * with this curve
	 * @return the point at which this curve intersects the boundary of 
	 * the given rectangle, if it does so. If it does not intersect, 
	 * -1 is returned
	 */
	protected int intersectRectPerimeterSeg(String index, mxRectangle rect)
	{
		return intersectRectPerimeterSeg(index, rect, 1);
	}

	/**
	 * Utility method to determine within which segment the specified rectangle
	 * intersects the specified curve. This method specifies which segment to
	 * start searching at.
	 * 
	 * @param index the curve index specifying the curve to analyse
	 * @param rect the whose boundary is to be tested for intersection
	 * with this curve
	 * @param startSegment the segment to start searching at. To start at the 
	 * 			beginning of the curve, use 1, not 0.
	 * @return the point at which this curve intersects the boundary of 
	 * the given rectangle, if it does so. If it does not intersect, 
	 * -1 is returned
	 */
	protected int intersectRectPerimeterSeg(String index, mxRectangle rect,
			int startSegment)
	{
		mxPoint[] pointsCurve = getCurvePoints(index);

		if (pointsCurve != null && pointsCurve.length > 1)
		{
			for (int i = startSegment; i < pointsCurve.length; i++)
			{
				if (rect.intersectLine(pointsCurve[i].getX(),
						pointsCurve[i].getY(), pointsCurve[i - 1].getX(),
						pointsCurve[i - 1].getY()) != null)
				{
					return i;
				}
			}
		}

		return -1;
	}

	/**
	 * Returns the point at which this curve segment intersects the boundary 
	 * of the given rectangle, if it does so. If it does not intersect, 
	 * null is returned.
	 * 
	 * @param curveIndex the curve index specifying the curve to analyse
	 * @param rect the whose boundary is to be tested for intersection
	 * with this curve
	 * @param indexSeg the segments on this curve being checked
	 * @return the point at which this curve segment  intersects the boundary 
	 * of the given rectangle, if it does so. If it does not intersect, 
	 * null is returned.
	 */
	protected mxPoint intersectRectPerimeterPoint(String curveIndex,
			mxRectangle rect, int indexSeg)
	{
		mxPoint result = null;
		mxPoint[] pointsCurve = getCurvePoints(curveIndex);

		if (pointsCurve != null && pointsCurve.length > 1 && indexSeg >= 0
				&& indexSeg < pointsCurve.length)
		{
			double p1X = pointsCurve[indexSeg - 1].getX();
			double p1Y = pointsCurve[indexSeg - 1].getY();
			double p2X = pointsCurve[indexSeg].getX();
			double p2Y = pointsCurve[indexSeg].getY();

			result = rect.intersectLine(p1X, p1Y, p2X, p2Y);
		}

		return result;
	}

	/**
	 * Calculates the position of an absolute in terms relative
	 * to this curve.
	 * 
	 * @param absPoint the point whose relative point is to calculated
	 * @param index the index of the curve whom the relative position is to be 
	 * calculated from
	 * @return an mxRectangle where the x is the distance along the curve 
	 * (0 to 1), y is the orthogonal offset from the closest segment on the 
	 * curve and (width, height) is an additional Cartesian offset applied
	 * after the other calculations
	 */
	public mxRectangle getRelativeFromAbsPoint(mxPoint absPoint, String index)
	{
		// Work out which segment the absolute point is closest to
		mxPoint[] currentCurve = getCurvePoints(index);
		double[] currentIntervals = getIntervals(index);
		if (currentCurve == null || currentIntervals == null)
		{
			throw new IllegalArgumentException("Invalid index: "+index+",  cannot find CurvePoints and/or Intervals");
		}
		int closestSegment = 0;
		double closestSegDistSq = 10000000;
		mxLine segment = new mxLine(currentCurve[0], currentCurve[1]);

		for (int i = 1; i < currentCurve.length; i++)
		{
			segment.setPoints(currentCurve[i - 1], currentCurve[i]);
			double segDistSq = segment.ptSegDistSq(absPoint);

			if (segDistSq < closestSegDistSq)
			{
				closestSegDistSq = segDistSq;
				closestSegment = i - 1;
			}
		}

		// Get the distance (squared) from the point to the
		// infinitely extrapolated line created by the closest
		// segment. If that value is the same as the distance
		// to the segment then an orthogonal offset from some
		// point on the line will intersect the point. If they
		// are not equal, an additional cartesian offset is
		// required
		mxPoint startSegPt = currentCurve[closestSegment];
		mxPoint endSegPt = currentCurve[closestSegment + 1];

		mxLine closestSeg = new mxLine(startSegPt, endSegPt);
		double lineDistSq = closestSeg.ptLineDistSq(absPoint);

		double orthogonalOffset = Math.sqrt(Math.min(lineDistSq,
				closestSegDistSq));
		double segX = endSegPt.getX() - startSegPt.getX();
		double segY = endSegPt.getY() - startSegPt.getY();
		double segDist = Math.sqrt(segX * segX + segY * segY);
		double segNormX = segX / segDist;
		double segNormY = segY / segDist;
		// The orthogonal offset could be in one of two opposite vectors
		// Try both solutions, one will be closer to one of the segment
		// end points (unless the point is on the line)
		double candidateOffX1 = (absPoint.getX() - segNormY * orthogonalOffset)
				- endSegPt.getX();
		double candidateOffY1 = (absPoint.getY() + segNormX * orthogonalOffset)
				- endSegPt.getY();
		double candidateOffX2 = (absPoint.getX() + segNormY * orthogonalOffset)
				- endSegPt.getX();
		double candidateOffY2 = (absPoint.getY() - segNormX * orthogonalOffset)
				- endSegPt.getY();

		double candidateDist1 = (candidateOffX1 * candidateOffX1)
				+ (candidateOffY1 * candidateOffY1);
		double candidateDist2 = (candidateOffX2 * candidateOffX2)
				+ (candidateOffY2 * candidateOffY2);

		double orthOffsetPointX = 0;
		double orthOffsetPointY = 0;

		if (candidateDist2 < candidateDist1)
		{
			orthogonalOffset = -orthogonalOffset;
		}

		orthOffsetPointX = absPoint.getX() - segNormY * orthogonalOffset;
		orthOffsetPointY = absPoint.getY() + segNormX * orthogonalOffset;

		double distAlongEdge = 0;
		double cartOffsetX = 0;
		double cartOffsetY = 0;

		// Don't compare for exact equality, there are often rounding errors
		if (Math.abs(closestSegDistSq - lineDistSq) > 0.0001)
		{
			// The orthogonal offset does not move the point onto the
			// segment. Work out an additional cartesian offset that moves
			// the offset point onto the closest end point of the
			// segment

			// Not exact distances, but the equation holds
			double distToStartPoint = Math.abs(orthOffsetPointX
					- startSegPt.getX())
					+ Math.abs(orthOffsetPointY - startSegPt.getY());
			double distToEndPoint = Math
					.abs(orthOffsetPointX - endSegPt.getX())
					+ Math.abs(orthOffsetPointY - endSegPt.getY());
			if (distToStartPoint < distToEndPoint)
			{
				distAlongEdge = currentIntervals[closestSegment];
				cartOffsetX = orthOffsetPointX - startSegPt.getX();
				cartOffsetY = orthOffsetPointY - startSegPt.getY();
			}
			else
			{
				distAlongEdge = currentIntervals[closestSegment + 1];
				cartOffsetX = orthOffsetPointX - endSegPt.getX();
				cartOffsetY = orthOffsetPointY - endSegPt.getY();
			}
		}
		else
		{
			// The point, when orthogonally offset, lies on the segment
			// work out what proportion along the segment, and therefore
			// the entire curve, the offset point lies.
			double segmentLen = Math.sqrt((endSegPt.getX() - startSegPt.getX())
					* (endSegPt.getX() - startSegPt.getX())
					+ (endSegPt.getY() - startSegPt.getY())
					* (endSegPt.getY() - startSegPt.getY()));
			double offsetLen = Math.sqrt((orthOffsetPointX - startSegPt.getX())
					* (orthOffsetPointX - startSegPt.getX())
					+ (orthOffsetPointY - startSegPt.getY())
					* (orthOffsetPointY - startSegPt.getY()));
			double proportionAlongSeg = offsetLen / segmentLen;
			double segProportingDiff = currentIntervals[closestSegment + 1]
					- currentIntervals[closestSegment];
			distAlongEdge = currentIntervals[closestSegment]
					+ segProportingDiff * proportionAlongSeg;
		}

		if (distAlongEdge > 1.0)
		{
			distAlongEdge = 1.0;
		}

		return new mxRectangle(distAlongEdge, orthogonalOffset, cartOffsetX,
				cartOffsetY);
	}

	/**
	 * Creates the core curve that is based on the guide points passed into
	 * this class instance
	 */
	protected void createCoreCurve()
	{
		// Curve is marked invalid until all of the error situations have
		// been checked
		valid = false;

		if (guidePoints == null || guidePoints.isEmpty())
		{
			return;
		}

		for (int i = 0; i < guidePoints.size(); i++)
		{
			if (guidePoints.get(i) == null)
			{
				return;
			}
		}

		// Reset the cached bounds value
		minXBounds = minYBounds = 10000000;
		maxXBounds = maxYBounds = 0;

		mxSpline spline = new mxSpline(guidePoints);

		// Need the rough length of the spline, so we can get
		// more samples for longer edges
		double lengthSpline = spline.getLength();

		// Check for errors in the spline calculation or zero length curves
		if (Double.isNaN(lengthSpline) || !spline.checkValues()
				|| lengthSpline < 1)
		{
			return;
		}

		mxSpline1D splineX = spline.getSplineX();
		mxSpline1D splineY = spline.getSplineY();
		double baseInterval = 12.0 / lengthSpline;
		double minInterval = 1.0 / lengthSpline;

		// Store the last two spline positions. If the next position is 
		// very close to where the extrapolation of the last two points 
		// then double the interval. This diviation is terms the "flatness".
		// There is a range where the interval is kept the same, any 
		// variation from this range of flatness invokes a proportional 
		// adjustment to try to reenter the range without 
		// over compensating
		double interval = baseInterval;
		// These deviations are only tested against either 
		// dimension individually, working out the correct 
		// distance is too computationally intensive
		double minDeviation = 0.15;
		double maxDeviation = 0.3;
		double preferedDeviation = (maxDeviation + minDeviation) / 2.0;

		// x1, y1 are the position two iterations ago, x2, y2
		// the position on the last iteration
		double x1 = -1.0;
		double x2 = -1.0;
		double y1 = -1.0;
		double y2 = -1.0;

		// Store the change in interval amount between iterations.
		// If it changes the extrapolation calculation must
		// take this into account.
		double intervalChange = 1;

		List<mxPoint> coreCurve = new ArrayList<mxPoint>();
		List<Double> coreIntervals = new ArrayList<Double>();
		boolean twoLoopsComplete = false;

		for (double t = 0; t <= 1.5; t += interval)
		{
			if (t > 1.0)
			{
				// Use the point regardless of the accuracy, 
				t = 1.0001;
				mxPoint endControlPoint = guidePoints
						.get(guidePoints.size() - 1);
				mxPoint finalPoint = new mxPoint(endControlPoint.getX(),
						endControlPoint.getY());
				coreCurve.add(finalPoint);
				coreIntervals.add(t);
				updateBounds(endControlPoint.getX(), endControlPoint.getY());
				break;
			}
			// Whether or not the accuracy of the current point is acceptable
			boolean currentPointAccepted = true;

			double newX = splineX.getFastValue(t);
			double newY = splineY.getFastValue(t);

			// Check if the last points are valid (indicated by
			// dissimilar values)
			// Check we're not in the first, second or last run
			if (x1 != -1.0 && twoLoopsComplete && t != 1.0001)
			{
				// Work out how far the new spline point
				// deviates from the extrapolation created 
				// by the last two points
				double diffX = Math.abs(((x2 - x1) * intervalChange + x2)
						- newX);
				double diffY = Math.abs(((y2 - y1) * intervalChange + y2)
						- newY);

				// If either the x or y of the straight line
				// extrapolation from the last two points
				// is more than the 1D deviation allowed
				// go back and re-calculate with a smaller interval
				// It's possible that the edge has curved too fast
				// for the algorithmn. If the interval is
				// reduced to less than the minimum permitted
				// interval, it may be that it's impossible
				// to get within the deviation because of
				// the extrapolation overshoot. The minimum 
				// interval is set to draw correctly for the
				// vast majority of cases.
				if ((diffX > maxDeviation || diffY > maxDeviation)
						&& interval != minInterval)
				{
					double overshootProportion = maxDeviation
							/ Math.max(diffX, diffY);

					if (interval * overshootProportion <= minInterval)
					{
						// Set the interval 
						intervalChange = minInterval / interval;
					}
					else
					{
						// The interval can still be reduced, half 
						// the interval and go back and redo
						// this iteration
						intervalChange = overshootProportion;
					}

					t -= interval;
					interval *= intervalChange;
					currentPointAccepted = false;
				}
				else if (diffX < minDeviation && diffY < minDeviation)
				{
					intervalChange = 1.4;
					interval *= intervalChange;
				}
				else
				{
					// Try to keep the deviation around the prefered value
					double errorRatio = preferedDeviation
							/ Math.max(diffX, diffY);
					intervalChange = errorRatio / 4.0;
					interval *= intervalChange;
				}

				if (currentPointAccepted)
				{
					x1 = x2;
					y1 = y2;
					x2 = newX;
					y2 = newY;
				}
			}
			else if (x1 == -1.0)
			{
				x1 = x2 = newX;
				y1 = y2 = newY;
			}
			else if (x1 == x2 && y1 == y2)
			{
				x2 = newX;
				y2 = newY;
				twoLoopsComplete = true;
			}
			if (currentPointAccepted)
			{
				mxPoint newPoint = new mxPoint(newX, newY);
				coreCurve.add(newPoint);
				coreIntervals.add(t);
				updateBounds(newX, newY);
			}
		}

		if (coreCurve.size() < 2)
		{
			// A single point makes no sense, leave the curve as invalid
			return;
		}

		mxPoint[] corePoints = new mxPoint[coreCurve.size()];
		int count = 0;

		for (mxPoint point : coreCurve)
		{
			corePoints[count++] = point;
		}

		points = new Hashtable<String, mxPoint[]>();
		curveLengths = new Hashtable<String, Double>();
		points.put(CORE_CURVE, corePoints);
		curveLengths.put(CORE_CURVE, lengthSpline);

		double[] coreIntervalsArray = new double[coreIntervals.size()];
		count = 0;

		for (Double tempInterval : coreIntervals)
		{
			coreIntervalsArray[count++] = tempInterval.doubleValue();
		}

		intervals = new Hashtable<String, double[]>();
		intervals.put(CORE_CURVE, coreIntervalsArray);

		valid = true;
	}

	/** Whether or not the label curve starts from the end target
	 *  and traces to the start of the branch
	 * @return whether the label curve is reversed
	 */
	public boolean isLabelReversed()
	{
		if (valid)
		{
			mxPoint[] centralCurve = getCurvePoints(CORE_CURVE);

			if (centralCurve != null)
			{
				double changeX = centralCurve[centralCurve.length - 1].getX()
						- centralCurve[0].getX();

				if (changeX < 0)
				{
					return true;
				}
			}
		}

		return false;
	}

	protected void createLabelCurve()
	{
		// Place the label on the "high" side of the vector
		// joining the start and end points of the curve
		mxPoint[] currentCurve = getBaseLabelCurve();
		if (currentCurve == null)
			return;

		boolean labelReversed = isLabelReversed();

		List<mxPoint> labelCurvePoints = new ArrayList<mxPoint>();

		// Lower and upper curve start from the very ends
		// of their curves, so given that their middle points
		// are derived from the center of the central points
		// they will contain one more point and both
		// side curves contain the same end point

		for (int i = 1; i < currentCurve.length; i++)
		{
			int currentIndex = i;
			int lastIndex = i - 1;

			if (labelReversed)
			{
				currentIndex = currentCurve.length - i - 1;
				lastIndex = currentCurve.length - i;
			}

			mxPoint segStartPoint = currentCurve[currentIndex];
			mxPoint segEndPoint = currentCurve[lastIndex];
			double segVectorX = segEndPoint.getX() - segStartPoint.getX();
			double segVectorY = segEndPoint.getY() - segStartPoint.getY();
			double segVectorLength = Math.sqrt(segVectorX * segVectorX
					+ segVectorY * segVectorY);
			double normSegVectorX = segVectorX / segVectorLength;
			double normSegVectorY = segVectorY / segVectorLength;
			double centerSegX = (segEndPoint.getX() + segStartPoint.getX()) / 2.0;
			double centerSegY = (segEndPoint.getY() + segStartPoint.getY()) / 2.0;

			if (i == 1)
			{
				// Special case to work out the very end points at
				// the start of the curve
				mxPoint startPoint = new mxPoint(segEndPoint.getX()
						- (normSegVectorY * labelBuffer), segEndPoint.getY()
						+ (normSegVectorX * labelBuffer));
				labelCurvePoints.add(startPoint);
				updateBounds(startPoint.getX(), startPoint.getY());
			}

			double pointX = centerSegX - (normSegVectorY * labelBuffer);
			double pointY = centerSegY + (normSegVectorX * labelBuffer);
			mxPoint labelCurvePoint = new mxPoint(pointX, pointY);
			updateBounds(pointX, pointY);
			labelCurvePoints.add(labelCurvePoint);

			if (i == currentCurve.length - 1)
			{
				// Special case to work out the very end points at
				// the start of the curve
				mxPoint endPoint = new mxPoint(segStartPoint.getX()
						- (normSegVectorY * labelBuffer), segStartPoint.getY()
						+ (normSegVectorX * labelBuffer));
				labelCurvePoints.add(endPoint);
				updateBounds(endPoint.getX(), endPoint.getY());
			}
		}

		mxPoint[] tmpPoints = new mxPoint[labelCurvePoints.size()];
		points.put(LABEL_CURVE, labelCurvePoints.toArray(tmpPoints));
		populateIntervals(LABEL_CURVE);
	}

	/**
	 * Returns the curve the label curve is too be based on
	 */
	protected mxPoint[] getBaseLabelCurve()
	{
		return getCurvePoints(CORE_CURVE);
	}

	protected void populateIntervals(String index)
	{
		mxPoint[] currentCurve = points.get(index);
		if (currentCurve == null)
			// do nothing
			return;

		double[] newIntervals = new double[currentCurve.length];

		double totalLength = 0.0;
		newIntervals[0] = 0;

		for (int i = 0; i < currentCurve.length - 1; i++)
		{
			double changeX = currentCurve[i + 1].getX()
					- currentCurve[i].getX();
			double changeY = currentCurve[i + 1].getY()
					- currentCurve[i].getY();
			double segLength = Math.sqrt(changeX * changeX + changeY * changeY);
			// We initially fill the intervals with the total distance to
			// the end of this segment then later normalize all the values
			totalLength += segLength;
			// The first index was populated before the loop (and is always 0)
			newIntervals[i + 1] = totalLength;
		}

		// Normalize the intervals
		for (int j = 0; j < newIntervals.length; j++)
		{
			if (j == newIntervals.length - 1)
			{
				// Make the final interval slightly over
				// 1.0 so any analysis to find the lower 
				newIntervals[j] = 1.0001;
			}
			else
			{
				newIntervals[j] = newIntervals[j] / totalLength;
			}
		}

		intervals.put(index, newIntervals);
		curveLengths.put(index, totalLength);
	}

	/**
	 * Updates the existing curve using the points passed in. 
	 * @param newPoints the new guide points
	 */
	public void updateCurve(List<mxPoint> newPoints)
	{
		boolean pointsChanged = false;

		// If any of the new points are null, ignore the list
		for (mxPoint point : newPoints)
		{
			if (point == null)
			{
				return;
			}
		}

		if (newPoints.size() != guidePoints.size())
		{
			pointsChanged = true;
		}
		else
		{
			// Check for a constant translation of all guide points. In that 
			// case apply the translation directly to all curves.
			// Also check whether all of the translations are trivial
			if (newPoints.size() == guidePoints.size() && newPoints.size() > 1
					&& guidePoints.size() > 1)
			{
				boolean constantTranslation = true;
				boolean trivialTranslation = true;
				mxPoint newPoint0 = newPoints.get(0);
				mxPoint oldPoint0 = guidePoints.get(0);
				double transX = newPoint0.getX() - oldPoint0.getX();
				double transY = newPoint0.getY() - oldPoint0.getY();

				if (Math.abs(transX) > 0.01 || Math.abs(transY) > 0.01)
				{
					trivialTranslation = false;
				}

				for (int i = 1; i < newPoints.size(); i++)
				{
					double nextTransX = newPoints.get(i).getX()
							- guidePoints.get(i).getX();
					double nextTransY = newPoints.get(i).getY()
							- guidePoints.get(i).getY();

					if (Math.abs(transX - nextTransX) > 0.01
							|| Math.abs(transY - nextTransY) > 0.01)
					{
						constantTranslation = false;
					}

					if (Math.abs(nextTransX) > 0.01
							|| Math.abs(nextTransY) > 0.01)
					{
						trivialTranslation = false;
					}
				}

				if (trivialTranslation)
				{
					pointsChanged = false;
				}
				else if (constantTranslation)
				{
					pointsChanged = false;
					// Translate all stored points by the translation amounts
					Collection<mxPoint[]> curves = points.values();

					// Update all geometry information held by the curve
					// That is, all the curve points, the guide points
					// and the cached bounds
					for (mxPoint[] curve : curves)
					{
						for (int i = 0; i < curve.length; i++)
						{
							curve[i].setX(curve[i].getX() + transX);
							curve[i].setY(curve[i].getY() + transY);
						}
					}

					guidePoints = new ArrayList<mxPoint>(newPoints);
					minXBounds += transX;
					minYBounds += transY;
					maxXBounds += transX;
					maxYBounds += transY;
				}
				else
				{
					pointsChanged = true;
				}
			}
		}

		if (pointsChanged)
		{
			guidePoints = new ArrayList<mxPoint>(newPoints);
			points = new Hashtable<String, mxPoint[]>();
			valid = false;
		}
	}

	/**
	 * Obtains the points that make up the curve for the specified
	 * curve index. If that curve, or the core curve that other curves
	 * are based on have not yet been created, then they are lazily
	 * created. If creation is impossible, null is returned
	 * @param index the key specifying the curve
	 * @return the points making up that curve, or null
	 */
	public mxPoint[] getCurvePoints(String index)
	{
		if (validateCurve())
		{
			if (points.get(LABEL_CURVE) == null && index == LABEL_CURVE)
			{
				createLabelCurve();
			}

			return points.get(index);
		}

		return null;
	}

	public double[] getIntervals(String index)
	{
		if (validateCurve())
		{
			if (points.get(LABEL_CURVE) == null && index == LABEL_CURVE)
			{
				createLabelCurve();
			}

			return intervals.get(index);
		}

		return null;
	}

	public double getCurveLength(String index)
	{
		if (validateCurve())
		{
			if (intervals.get(index) == null)
			{
				createLabelCurve();
			}

			return curveLengths.get(index);
		}

		return 0;
	}

	/**
	 * Method must be called before any attempt to access curve information
	 * @return whether or not the curve may be used
	 */
	protected boolean validateCurve()
	{
		if (!valid)
		{
			createCoreCurve();
		}

		return valid;
	}

	/**
	 * Updates the total bounds of this curve, increasing any dimensions,
	 * if necessary, to fit in the specified point
	 */
	protected void updateBounds(double pointX, double pointY)
	{
		minXBounds = Math.min(minXBounds, pointX);
		maxXBounds = Math.max(maxXBounds, pointX);
		minYBounds = Math.min(minYBounds, pointY);
		maxYBounds = Math.max(maxYBounds, pointY);
	}

	/**
	 * @return the guidePoints
	 */
	public List<mxPoint> getGuidePoints()
	{
		return guidePoints;
	}
}
