/*
 * Created on 12-Feb-2004
 *
 * To change the template for this generated file go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
package pipe.dataLayer;

import java.awt.BasicStroke;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;

import pipe.gui.ArcPathPointHandler;
import pipe.gui.Constants;
import pipe.gui.GuiView;

/**
 * @author Peter Kyme, Tom Barnwell and Michael Camacho
 *
 * To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Generation - Code and Comments
 */
public class ArcPath implements Shape, Cloneable, Constants {
	
//	private static final int CONTROL_POINT_CONSTANT = 2;
//	private static final int PATH_SELECTION_RADIUS = 6;
	private GeneralPath path = new GeneralPath();
	private GeneralPath arcSelection;
	private GeneralPath pointSelection;
	private List pathPoints = new ArrayList();
	private Arc myArc;
	ArcPathPoint currentPoint;
	private boolean pointLock = false;
	private static Stroke proximityStroke = new BasicStroke(ARC_PATH_PROXIMITY_WIDTH/*, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER*/);
	private static Stroke stroke = new BasicStroke(ARC_PATH_SELECTION_WIDTH);
	private Shape shape, proximityShape;
	private int transitionAngle;
	private boolean showControlPoints = false;
	
	public ArcPath(Arc a){
		myArc = a;
		transitionAngle = 0;
	}
	
	public void createPath() {
		
		setControlPoints();
		
		currentPoint = null;
		path = new GeneralPath();
		currentPoint = (ArcPathPoint)pathPoints.get(0);
		path.moveTo(currentPoint.getPoint().x, currentPoint.getPoint().y);
		
		getArc().setWeightLabelPosition();
		
		currentPoint.setPointType(ArcPathPoint.STRAIGHT);
		
		for (int c = 1; c <= getEndIndex(); c++) {
			
			currentPoint = (ArcPathPoint)pathPoints.get(c);
			
			if (currentPoint.getPointType() == ArcPathPoint.STRAIGHT){
				path.lineTo(currentPoint.getPoint().x, currentPoint.getPoint().y); 
			}
			else if (currentPoint.getPointType() == ArcPathPoint.CURVED){
				if (showControlPoints){//draw control lines for illustrative purposes
					path.lineTo(currentPoint.getControl1().x,currentPoint.getControl1().y);
					path.lineTo(currentPoint.getControl2().x,currentPoint.getControl2().y);
					path.lineTo(currentPoint.getPoint().x, currentPoint.getPoint().y);
					path.moveTo(((ArcPathPoint)pathPoints.get(c-1)).getPoint().x, ((ArcPathPoint)pathPoints.get(c-1)).getPoint().y);
				}
				path.curveTo(currentPoint.getControl1().x,currentPoint.getControl1().y,currentPoint.getControl2().x,currentPoint.getControl2().y,currentPoint.getPoint().x,currentPoint.getPoint().y);
			}

		}
		shape = stroke.createStrokedShape(this);
		proximityShape = proximityStroke.createStrokedShape(this);
	}
	
	private void setControlPoints(){
		setCurveControlPoints(); //must be in this order
		setStraightControlPoints();
		setEndControlPoints();
	}
	
	/*        returns a control point for curve CD with incoming vector AB*/
	private Point2D.Float getControlPoint(Point2D.Float A, Point2D.Float B, Point2D.Float C, Point2D.Float D){
		Point2D.Float p = new Point2D.Float(0, 0);
		
		double modAB = getMod(A, B);
		double modCD = getMod(C, D);		
		
		double ABx = (B.x - A.x) / modAB; 
		double ABy = (B.y - A.y) / modAB;
		
		if (modAB < 7) 	// hack, stops division by zero, modAB can only be this low
						// if the points are virtually superimposed anyway
			p = (Point2D.Float)C.clone();	
		else{
			p.x = C.x + (float) (ABx * modCD / ARC_CONTROL_POINT_CONSTANT );
			p.y = C.y + (float) (ABy * modCD / ARC_CONTROL_POINT_CONSTANT );
		}
		return p;
	}	

	private double getMod(Point2D.Float A, Point2D.Float B){
		double ABx = A.x - B.x; 
		double ABy = A.y - B.y; 
		
		return Math.sqrt(ABx*ABx + ABy*ABy);
	}
	/* function sets control points for any curved sections of the path */
	private void setCurveControlPoints(){
    if(pathPoints.size()<1) return;
		ArcPathPoint myCurrentPoint = (ArcPathPoint)pathPoints.get(0);		
		myCurrentPoint.setPointType(ArcPathPoint.STRAIGHT);	
		
//		ArcPathPoint myPreviousButOnePoint = null;
//		ArcPathPoint myNextPoint = null;
//		ArcPathPoint myPreviousPoint = null;

		Cubic[] X,Y;
		
		int endIndex = getEndIndex();

		for (int c=1; c <= endIndex;){
			int curveStartIndex = 0;
			int curveEndIndex = 0;
			myCurrentPoint = (ArcPathPoint)pathPoints.get(c);
						
			if (myCurrentPoint.getPointType()==true){
				curveStartIndex = c-1;
				
				for(; c<= endIndex && myCurrentPoint.getPointType()==true; c++){
					myCurrentPoint = (ArcPathPoint)pathPoints.get(c);
					curveEndIndex = c;
				}
				/* calculate a cubic for each section of the curve */
				int lengthOfCurve = curveEndIndex - curveStartIndex;
				int k1;
				int x[] = new int[lengthOfCurve + 2];
				int y[] = new int[lengthOfCurve + 2];
				X = new Cubic[lengthOfCurve + 2];
				Y = new Cubic[lengthOfCurve + 2];
				
				for (k1= 0; k1 <= (curveEndIndex - curveStartIndex); k1++) {
					x[k1] = (int)((ArcPathPoint)pathPoints.get(curveStartIndex + k1)).getPoint().x;
					y[k1] = (int)((ArcPathPoint)pathPoints.get(curveStartIndex + k1)).getPoint().y;
				}
				x[k1] = x[k1-1];
				y[k1] = y[k1-1];

				X = calcNaturalCubic(k1,x);
				Y = calcNaturalCubic(k1,y);
				
				for (int k2 = 1; k2 <= lengthOfCurve; k2++){
					myCurrentPoint = (ArcPathPoint)pathPoints.get(k2 + curveStartIndex);
					myCurrentPoint.setControl1(X[k2-1].getX1(),Y[k2-1].getX1());
					myCurrentPoint.setControl2(X[k2-1].getX2(),Y[k2-1].getX2());
					
				}

			}
			
			else {
				c++;
			}
		}			
	}

	/* fuction sets the control points for any straight sections
	 * and for smooth intersection between straight and curved sections */
	private void setStraightControlPoints() {
		ArcPathPoint myCurrentPoint = (ArcPathPoint)pathPoints.get(0);		
		ArcPathPoint myPreviousButOnePoint = null;
		ArcPathPoint myNextPoint = null;
		ArcPathPoint myPreviousPoint = null;

		for (int c=1; c<=getEndIndex(); c++){
			myPreviousPoint = (ArcPathPoint)pathPoints.get(c-1);
			myCurrentPoint = (ArcPathPoint)pathPoints.get(c);
			
			if (myCurrentPoint.getPointType() == false){
				myCurrentPoint.setControl1(getControlPoint(myPreviousPoint.getPoint(),
						myCurrentPoint.getPoint(),
						myPreviousPoint.getPoint(),
						myCurrentPoint.getPoint()));
				myCurrentPoint.setControl2(getControlPoint(myCurrentPoint.getPoint(),
						myPreviousPoint.getPoint(),
						myCurrentPoint.getPoint(),
						myPreviousPoint.getPoint()));
			}
			
			else {
				if (c>1 && myPreviousPoint.getPointType() == false){
					
					myPreviousButOnePoint = (ArcPathPoint)pathPoints.get(c-2);
					myCurrentPoint.setControl1(getControlPoint(myPreviousButOnePoint.getPoint(),
							myPreviousPoint.getPoint(),
							myPreviousPoint.getPoint(),
							myCurrentPoint.getPoint()));
				}
				if (c<getEndIndex()){
					myNextPoint = (ArcPathPoint)pathPoints.get(c+1);
					if (myNextPoint.getPointType() == false){
						myCurrentPoint.setControl2(getControlPoint(myNextPoint.getPoint(),
								myCurrentPoint.getPoint(),
								myCurrentPoint.getPoint(),
								myPreviousPoint.getPoint()));
					}
				}
			}
		}
	}
	private void setEndControlPoints() {

//		Transition endTransition;
		PlaceTransitionObject source = getArc().getSource();
		PlaceTransitionObject target = getArc().getTarget();
		double anAngle = Math.toRadians(transitionAngle);
		
		if  (!(getEndIndex() > 0))
			return;
		else if (source != null && source instanceof Transition && 
				((ArcPathPoint)pathPoints.get(1)).getPointType() == true) {

			ArcPathPoint myPoint = (ArcPathPoint)pathPoints.get(1);
			ArcPathPoint myLastPoint = (ArcPathPoint)pathPoints.get(0);
			float distance = (float)getMod(myPoint.getPoint(), myLastPoint.getPoint())/ARC_CONTROL_POINT_CONSTANT;
			myPoint.setControl1((float)(myLastPoint.getPoint().x + Math.cos(anAngle)*distance),
								(float)(myLastPoint.getPoint().y + Math.sin(anAngle)*distance) );

			myPoint = (ArcPathPoint)pathPoints.get(getEndIndex());
			myPoint.setControl2(getControlPoint(myPoint.getPoint(),
					myPoint.getControl1(),
					myPoint.getPoint(),
					myPoint.getControl1()));
		}
		else if (target != null && source instanceof Place&& 
				((ArcPathPoint)pathPoints.get(getEndIndex())).getPointType() == true){

			ArcPathPoint myPoint = (ArcPathPoint)pathPoints.get(getEndIndex());
			ArcPathPoint myLastPoint = (ArcPathPoint)pathPoints.get(getEndIndex()-1);
			float distance = (float)getMod(myPoint.getPoint(), myLastPoint.getPoint())/ARC_CONTROL_POINT_CONSTANT;
			myPoint.setControl2((float)(myPoint.getPoint().x + Math.cos(anAngle)*distance),
					(float)(myPoint.getPoint().y + Math.sin(anAngle)*distance) );
			
			myPoint = (ArcPathPoint)pathPoints.get(1);
			myPoint.setControl1(getControlPoint(((ArcPathPoint)pathPoints.get(0)).getPoint(),
					myPoint.getControl2(),
					((ArcPathPoint)pathPoints.get(0)).getPoint(),
					myPoint.getControl2()));

			
		}
	}
		
	public GeneralPath getPath() {
		return path;
	}
	
	public GeneralPath getArcSelection() {
		return arcSelection;
	}
	
	public GeneralPath getPointSelection() {
		return pointSelection;
	}
	public void addPoint(float x, float y, boolean type) {
		pathPoints.add(new ArcPathPoint(x, y, type, this));		
	}

	public void addPoint(double x, double y, boolean type) {
		pathPoints.add(new ArcPathPoint((float)x, (float)y, type, this));		
	}
	public void addPoint() {
		pathPoints.add(new ArcPathPoint(this));
	}
	
	public void deletePoint(ArcPathPoint a)
	{
		pathPoints.remove(a);
	}
	
	public void updateArc()
	{
		myArc.updateArcPosition();
	}
	
	
	public void translatePoint(int index, float x, float y) {
		ArcPathPoint point = (ArcPathPoint)pathPoints.get(index);
		point.setPointLocation(point.getPoint().x+x,point.getPoint().y+y);
	}
	
	/* (non-Javadoc)
	 * @see java.awt.Shape#contains(double, double)
	 */
	public boolean contains(double arg0, double arg1) {
		// TODO Auto-generated method stub
		return false;
	}
	
	public int getEndIndex() {
		return pathPoints.size()-1;
	}
	
//	public void setPointLocation(int index, float x, float y) {
//		((ArcPathPoint)pathPoints.get(index)).setPointLocation(x,y);
//	}

	public void setPointLocation(int index, double x, double y) {
    if(index<pathPoints.size() && index>=0)
		((ArcPathPoint)pathPoints.get(index)).setPointLocation((float)x,(float)y);
	}
	
	public void setPointLocation(int index, Point2D.Double point) {
		((ArcPathPoint)pathPoints.get(index)).setPointLocation((float)point.x,(float)point.y);
	}
	public void setPointType(int index, boolean type) {
		((ArcPathPoint)pathPoints.get(index)).setPointType(type);
	}
	
	public void setFinalPointType(boolean type) {
		((ArcPathPoint)pathPoints.get(getEndIndex())).setPointType(type);
	}
	
	public void togglePointType(int index) {
		((ArcPathPoint)pathPoints.get(index)).togglePointType();
	}
	
	public boolean isPointSelected(int index) {
		return ((ArcPathPoint)pathPoints.get(index)).isSelected();
	}
	
	public void selectPoint(int index) {
		((ArcPathPoint)pathPoints.get(index)).select();
	}
	
	public void deselectPoint(int index) {
		((ArcPathPoint)pathPoints.get(index)).deselect();
	}
	
	public int getNumPoints() {
		return pathPoints.size();
	}
	
	
	public Point2D.Float getPoint(int index) {
		return ((ArcPathPoint)pathPoints.get(index)).getPoint();
	}
	
	public ArcPathPoint getPathPoint(int index) {
		return ((ArcPathPoint)pathPoints.get(index));
	}
	
	public Arc getArc() {
		return myArc;
	}
	
	public void showPoints() {
		if (!pointLock)
		{
			for (int i=0; i<pathPoints.size(); i++)
				((ArcPathPoint)pathPoints.get(i)).setVisible(true);
		}
	}
	
	public void hidePoints() {
		if (!pointLock)
		{
			for (int i=0; i<pathPoints.size(); i++)
			{
				currentPoint = ((ArcPathPoint)pathPoints.get(i));
				if (!currentPoint.isSelected())
					currentPoint.setVisible(false);
			}
		}
	}
	
	/* (non-Javadoc)
	 * @see java.awt.Shape#intersects(java.awt.geom.Rectangle2D)
	 */
	public void setPointVisibilityLock(boolean lock) {
		pointLock = lock;
	}	

	
//	public double getEndAngle() {
//		if (getEndIndex()>0)
//			return ((ArcPathPoint)pathPoints.get(getEndIndex())).getAngle((ArcPathPoint)(pathPoints.get(getEndIndex()-1)));
//		return 0;
//	}
//	
//	public double getStartAngle() {
//		if (getEndIndex()>0)
//			return ((ArcPathPoint)pathPoints.get(0)).getAngle((ArcPathPoint)(pathPoints.get(1)));
//		return 0;
//	}
	
	/* modified to use control points, ensures a curve hits a place tangetially */
	public double getEndAngle() {
		if (getEndIndex()>0){
			if (getArc().getTarget() instanceof Transition)
				return ((ArcPathPoint)pathPoints.get(getEndIndex())).getAngle(((ArcPathPoint)(pathPoints.get(getEndIndex()))).getControl2());
			else
				return ((ArcPathPoint)pathPoints.get(getEndIndex())).getAngle(((ArcPathPoint)(pathPoints.get(getEndIndex()))).getControl1());
		}
		return 0;
	}
	
	public double getStartAngle() {
		if (getEndIndex()>0)
			return ((ArcPathPoint)pathPoints.get(0)).getAngle(((ArcPathPoint)(pathPoints.get(1))).getControl2());
		return 0;
	}
	/* (non-Javadoc)
	 * @see java.awt.Shape#contains(double, double, double, double)
	 */
	public boolean contains(
		double arg0,
		double arg1,
		double arg2,
		double arg3) {
		// TODO Auto-generated method stub
		return false;
	}

	/* (non-Javadoc)
	 * @see java.awt.Shape#intersects(double, double, double, double)
	 */
	public boolean intersects(
		double arg0,
		double arg1,
		double arg2,
		double arg3) {
		// TODO Auto-generated method stub
		return false;
	}

	/* (non-Javadoc)
	 * @see java.awt.Shape#getBounds()
	 */
	public Rectangle getBounds() {
		// TODO Auto-generated method stub
		return path.getBounds();
	}

	/* (non-Javadoc)
	 * @see java.awt.Shape#contains(java.awt.geom.Point2D)
	 */
	public boolean contains(Point2D p) {
		return shape.contains(p);
	} 
	/* (non-Javadoc)
	 * @see java.awt.Shape#contains(java.awt.geom.Point2D)
	 */
	public boolean proximityContains(Point2D p) {
		return proximityShape.contains(p);
	} 	
	/* (non-Javadoc)
	 * @see java.awt.Shape#getBounds2D()
	 */
	public Rectangle2D getBounds2D() {
		// TODO Auto-generated method stub
		return null;
	}

	/* (non-Javadoc)
	 * @see java.awt.Shape#contains(java.awt.geom.Rectangle2D)
	 */
	public boolean contains(Rectangle2D arg0) {
		// TODO Auto-generated method stub
		return false;
	}

	/* (non-Javadoc)
	 * @see java.awt.Shape#intersects(java.awt.geom.Rectangle2D)
	 */
	public boolean intersects(Rectangle2D r) {
		return shape.intersects(r);
	}
	/* (non-Javadoc)
	 * @see java.awt.Shape#intersects(java.awt.geom.Rectangle2D)
	 */
	public boolean proximityIntersects(Rectangle2D r) {
		return proximityShape.intersects(r);
	}	
	
	/* (non-Javadoc)
	 * @see java.awt.Shape#getPathIterator(java.awt.geom.AffineTransform)
	 */
	public PathIterator getPathIterator(AffineTransform arg0) {
		return path.getPathIterator(arg0);
	}

	/* (non-Javadoc)
	 * @see java.awt.Shape#getPathIterator(java.awt.geom.AffineTransform, double)
	 */
	public PathIterator getPathIterator(AffineTransform arg0, double arg1) {
		return path.getPathIterator(arg0, arg1);
	}

	public Cubic[] calcNaturalCubic(int n, int[] x) {
		float[] gamma = new float[n+1];
		float[] delta = new float[n+1];
		float[] D = new float[n+1];
		int i;
		/* We solve the equation
		 [2 1       ] [D[0]]   [3(x[1] - x[0])  ]
		 |1 4 1     | |D[1]|   |3(x[2] - x[0])  |
		 |  1 4 1   | | .  | = |      .         |
		 |    ..... | | .  |   |      .         |
		 |     1 4 1| | .  |   |3(x[n] - x[n-2])|
		 [       1 2] [D[n]]   [3(x[n] - x[n-1])]
		 
		 by using row operations to convert the matrix to upper triangular
		 and then back sustitution.  The D[i] are the derivatives at the knots.
		 */
		
		gamma[0] = 1.0f/2.0f;
		for ( i = 1; i < n; i++) {
			gamma[i] = 1/(4-gamma[i-1]);
		}
		gamma[n] = 1/(2-gamma[n-1]);
		
		delta[0] = 3*(x[1]-x[0])*gamma[0];
		for ( i = 1; i < n; i++) {
			delta[i] = (3*(x[i+1]-x[i-1])-delta[i-1])*gamma[i];
		}
		delta[n] = (3*(x[n]-x[n-1])-delta[n-1])*gamma[n];
		
		D[n] = delta[n];
		for ( i = n-1; i >= 0; i--) {
			D[i] = delta[i] - gamma[i]*D[i+1];
		}
		
		/* now compute the coefficients of the cubics */
		Cubic[] C = new Cubic[n];
		for ( i = 0; i < n; i++) {
			C[i] = new Cubic(x[i], D[i], 3*(x[i+1] - x[i]) - 2*D[i] - D[i+1],
					2*(x[i] - x[i+1]) + D[i] + D[i+1]);
		}
		return C;
	}
	
	
	
	public void addPointsToGui(GuiView editWindow) {
		ArcPathPoint pathPoint;
		((ArcPathPoint)pathPoints.get(0)).setDraggable(false);
		((ArcPathPoint)pathPoints.get(pathPoints.size()-1)).setDraggable(false);

		ArcPathPointHandler pointHandler;
		for (int i=0; i<pathPoints.size(); i++)
		{
			pathPoint = (ArcPathPoint)pathPoints.get(i);
			pathPoint.setVisible(false);
			editWindow.add(pathPoint);
			pointHandler = new ArcPathPointHandler(editWindow, pathPoint);
			pathPoint.addMouseListener(pointHandler);
			pathPoint.addMouseMotionListener(pointHandler);
			pathPoint.updatePointLocation();
		}
	}
	
	public void delete() {						// Michael: Tells the arc points to remove themselves
		while (!pathPoints.isEmpty()){
			((ArcPathPoint)pathPoints.get(0)).kill();	// force delete of ALL points
		}
	  }
	
	public String[][] getArcPathDetails(){
		
		int length = getEndIndex() + 1;
		
		String[][] details = new String[length][3];
		
		for (int c = 0; c < length; c++){
			
			details[c][0] = String.valueOf(((ArcPathPoint)pathPoints.get(c)).getX());
			details[c][1] = String.valueOf(((ArcPathPoint)pathPoints.get(c)).getY());
			details[c][2] = String.valueOf(((ArcPathPoint)pathPoints.get(c)).getPointType());
		}
		return details;
	}
	
	public void purgePathPoints()	// Dangerous! Only called from DataLayer when loading ArcPaths
	{
		pathPoints.clear();
	}
	
	public void setTransitionAngle(int angle){
		transitionAngle = angle;
		transitionAngle %= 360;
	}
}



class Cubic {
	
	float a,b,c,d;         /* a + b*u + c*u^2 +d*u^3 */
	
	public Cubic(float a, float b, float c, float d){
		this.a = a;
		this.b = b;
		this.c = c;
		this.d = d;
	}
	
	// Return first control point coordinate (calculated from coefficients)
	public float getX1() {
		return ((b+3*a)/3);
	}
	
	// Return second control point coordinate (calculated from coefficients)
	public float getX2() {
		return ((c+2*b+3*a)/3);
	}
	
	/** evaluate cubic */
	public float eval(float u) {
		return (((d*u) + c)*u + b)*u + a;
	}
	
}
