/*******************************************************************************
 * Copyright 2005-2006, CHISEL Group, University of Victoria, Victoria, BC,
 * Canada. 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: The Chisel Group, University of Victoria
 *******************************************************************************/
package org.eclipse.zest.core.widgets.internal;

import org.eclipse.draw2d.PolylineConnection;
import org.eclipse.draw2d.RectangleFigure;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;

/*
 * A connection that draws an arc between nodes, based on a given depth for the
 * arc. This connection is drawn as an arc, defined as the circular arc with the
 * chord (ax, ay) - (bx, by) (where a and b are the anchors) and a depth d
 * defined as the maximum distance from any point on the chord (i.e. a vector
 * normal to the chord with magnitude d).
 * 
 * @author Del Myers
 */
// @tag zest(bug(154391-ArcEnds(fix))) : force the endpoints to match by using a
// polyline connection.
// This will be more accurate than the regular ArcConnection, but it may be
// slower.
public class PolylineArcConnection extends PolylineConnection {
	private int depth;
	private boolean inverse = false;
	private static final float PI = (float) 3.14159;
	private RectangleFigure center;

	{
		this.depth = 0;
		center = new RectangleFigure();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.draw2d.Polyline#setPoints(org.eclipse.draw2d.geometry.PointList)
	 */
	public void setPoints(PointList points) {
		updateArc(points);
	}

	/**
	 * This method is not supported by this kind of connection. Points are
	 * calculated based on the arc definition.
	 */
	public void addPoint(Point pt) {
	}

	/**
	 * @param depth
	 *            the depth to set
	 */
	public void setDepth(int depth) {
		this.inverse = depth < 0 ? true : false;
		this.depth = depth;
		updateArc(getPoints());
	}

	protected void updateArc(PointList pointList) {
		if (pointList.size() < 2) {
			return;
		}
		if (center.getParent() == this) {
			remove(center);
		}
		Point start = pointList.getFirstPoint();
		Point end = pointList.getLastPoint();
		if (depth == 0) {
			super.setPoints(pointList);
			return;
		}

		PointList points = new PointList();

		float arcStart = 0;
		float arcEnd = 0;
		float arcLength = 0;
		float cartCenterX = 0;
		float cartCenterY = 0;
		float r = 0;

		float x1 = start.x;
		float y1 = -start.y;
		float x2 = end.x;
		float y2 = -end.y;
		float depth = this.depth;

		if (start.equals(end)) {
			// do a circle
			arcStart = -PI / 2;
			arcLength = PI * 2;
			// @tag drawing(arcs) : try making the center on a line from the
			// center of the parent figure.

			cartCenterX = x1;
			cartCenterY = y1 + depth / 2;
			r = depth / 2;

		} else {
			if (x1 >= x2) {
				depth = -depth;
			}
			// the center of the chord
			float cartChordX = (x2 + x1) / 2;
			float cartChordY = (y2 + y1) / 2;
			float chordLength = (float) Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
			if (Math.abs(depth) >= chordLength / 2) {
				depth = (chordLength / 3) * (depth / Math.abs(depth));
			}
			r = ((((chordLength / 2) * (chordLength / 2)) + (depth * depth)) / (2 * depth));

			// Find a vector normal to the chord. This will be used for
			// translating the
			// circle back to screen coordinates.
			float chordNormal = 0;

			if (Math.abs(x1 - x2) <= .000001) {
				// slope of 0. NaN is easier to detect than 0.
				chordNormal = Float.NaN;
			} else if (Math.abs(y1 - y2) <= 0.000001) {
				// infinite slope.
				chordNormal = Float.POSITIVE_INFINITY;
			} else {
				chordNormal = -1 * (y2 - y1) / (x2 - x1);
			}

			float th1;
			if (Float.isNaN(chordNormal)) {
				cartCenterX = (y1 > y2) ? (cartChordX - r + (depth)) : (cartChordX + r - (depth));
				cartCenterY = cartChordY;
				th1 = PI / 2;
			} else if (Float.isInfinite(chordNormal)) {
				cartCenterX = cartChordX;
				cartCenterY = cartChordY + r - (depth);
				th1 = 0;
			} else {
				// assume that the center of the chord is on the origin.
				th1 = (float) Math.atan(chordNormal);
				cartCenterX = (r - (depth)) * (float) Math.sin(th1) + cartChordX;// cartChordX+r
				// -depth;
				cartCenterY = (r - (depth)) * (float) Math.cos(th1) + cartChordY;// cartChordY+r-depth;

			}
			// figure out the new angles
			// translate the points to the center of the circle
			float cartArcX1 = x1 - cartCenterX;
			float cartArcY1 = y1 - cartCenterY;
			float cartArcX2 = x2 - cartCenterX;
			float cartArcY2 = y2 - cartCenterY;

			// calculate the length of the arc
			arcStart = angleRadians(cartArcX1, cartArcY1);
			arcEnd = angleRadians(cartArcX2, cartArcY2);

			if (arcEnd < arcStart) {
				arcEnd = arcEnd + PI + PI;
			}

			// make sure that we are between the two nodes.
			arcLength = arcEnd - arcStart;
			float pad = PI / Math.abs(r);
			arcStart += pad;
			arcEnd -= pad;
			arcLength = (arcEnd) - (arcStart);
			if (inverse) {
				arcLength = (2 * PI - arcLength);
			}
		}
		// calculate the points
		r = Math.abs(r);
		float x = 0, y = 0;
		Point p = null;
		points.addPoint(start);
		float length = arcLength * r;

		int steps = (int) length / 16;
		if (steps < 10 && length > 10) {
			steps = 10;
		}
		if (arcLength < PI / 4 && steps > 6) {
			steps = 6;
		}
		if (steps < 4 && length > 4) {
			steps = 4;
		}
		float stepSize = arcLength / steps;
		if (inverse) {
			float step = arcStart - stepSize;
			for (int i = 1; i < steps; i++, step -= stepSize) {
				x = (r) * (float) Math.cos(step) + cartCenterX;
				y = (r) * (float) Math.sin(step) + cartCenterY;
				p = new Point(Math.round(x), Math.round(-y));
				points.addPoint(p);
			}
		} else {
			float step = stepSize + arcStart;
			for (int i = 1; i < steps; i++, step += stepSize) {
				x = (r) * (float) Math.cos(step) + cartCenterX;
				y = (r) * (float) Math.sin(step) + cartCenterY;
				p = new Point(Math.round(x), Math.round(-y));
				points.addPoint(p);
			}
		}
		points.addPoint(end);

		super.setPoints(points);
	}

	/*
	 * Gets an angle in radians for the x, y coordinates. The angle will be
	 * between 0 and 2PI.
	 */
	float angleRadians(float x, float y) {
		float theta = (float) Math.atan(y / x);
		switch (findQuadrant(x, y)) {
		case 1:
			return theta;
		case 2:
			return (theta + PI);
		case 4:
			theta = (theta + PI);
		case 3:
			return (theta + PI);
		default:
			return theta;
		}

	}

	// find the quadrant, assume points are centered at 0,0
	protected int findQuadrant(float x, float y) {
		if (y > 0) {
			if (x > 0) {
				return 1;
			} else {
				return 2;
			}
		} else {
			if (x > 0) {
				return 4;
			} else {
				return 3;
			}
		}
	}
}
