/**
 * This software is released as part of the Pumpernickel project.
 * 
 * All com.pump resources in the Pumpernickel project are distributed under the
 * MIT License:
 * https://github.com/mickleness/pumpernickel/raw/master/License.txt
 * 
 * More information about the Pumpernickel project is available here:
 * https://mickleness.github.io/pumpernickel/
 */
package com.pump.geom;

import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;

public abstract class PathSegment {

	public int type;

	/**
	 * This maps the type integer to a human-readable string. This is intended
	 * for debugging and exceptions.
	 * 
	 * @param type
	 *            one of the PathIterator "SEG" constants.
	 * @return a string such as "SEG_CLOSE" or "SEG_LINETO".
	 */
	public static final String toTypeName(int type) {
		if (type == PathIterator.SEG_CLOSE)
			return "SEG_CLOSE";
		if (type == PathIterator.SEG_CUBICTO)
			return "SEG_CUBICTO";
		if (type == PathIterator.SEG_LINETO)
			return "SEG_LINETO";
		if (type == PathIterator.SEG_MOVETO)
			return "SEG_MOVETO";
		if (type == PathIterator.SEG_QUADTO)
			return "SEG_QUADTO";
		return "UNKNOWN";
	}

	public abstract void rotate(float f);

	public static class Float extends PathSegment {
		public float data[];
		public PathSegment.Float next;
		public PathSegment.Float prev;
		private float[] xCoeffs;
		private float[] yCoeffs;

		/**
		 * A very small value that is considered "equivalent to zero" in some
		 * operations where machine error may occur.
		 */
		protected static final float ZERO = .01f;

		/**
		 * Initializes a SEG_MOVETO segment.
		 * 
		 * This is the only publicly available constructor: to add other
		 * segments you should use the methods lineTo, cubicTo, etc.
		 * 
		 * @param moveX
		 * @param moveY
		 */
		public Float(float moveX, float moveY) {
			type = PathIterator.SEG_MOVETO;
			data = new float[] { moveX, moveY };
		}

		/**
		 * This constructor does not initialize anything.
		 */
		protected Float() {
		}

		/**
		 * Subclasses must override this method to return a segment of the
		 * correct type.
		 */
		protected Float newSegment() {
			return new Float();
		}

		public float[] getXCoeffs() {
			return getXCoeffs(null);
		}

		public float[] getXCoeffs(AffineTransform transform) {
			if (prev == null) {
				System.err.println(this);
				throw new NullPointerException("prev was null");
			}
			if (prev.data == null) {
				System.err.println(this);
				throw new NullPointerException("prev.data was null");
			}
			if (transform != null && transform.isIdentity())
				transform = null;
			if (xCoeffs != null && transform == null)
				return xCoeffs;

			double[] last = new double[] { prev.data[prev.data.length - 2],
					prev.data[prev.data.length - 1] };
			if (transform != null) {
				transform.transform(last, 0, last, 0, 1);
			}
			float[] myData;
			if (transform != null) {
				myData = new float[data.length];
				transform.transform(data, 0, myData, 0, data.length / 2);
			} else {
				myData = data;
			}
			float[] rValue = null;
			if (type == PathIterator.SEG_CUBICTO) {
				rValue = new float[4];
				rValue[0] = (float) (-last[0] + 3 * myData[0] - 3 * myData[2]
						+ myData[4]);
				rValue[1] = (float) (3 * last[0] - 6 * myData[0]
						+ 3 * myData[2]);
				rValue[2] = (float) (-3 * last[0] + 3 * myData[0]);
				rValue[3] = (float) (last[0]);
			} else if (type == PathIterator.SEG_QUADTO) {
				rValue = new float[3];
				rValue[0] = (float) (last[0] - 2 * myData[0] + myData[2]);
				rValue[1] = (float) (-2 * last[0] + 2 * myData[0]);
				rValue[2] = (float) (last[0]);
			} else if (type == PathIterator.SEG_LINETO) {
				rValue = new float[2];
				rValue[0] = (float) (-last[0] + myData[0]);
				rValue[1] = (float) (last[0]);
			} else if (type == PathIterator.SEG_MOVETO) {
				throw new UnsupportedOperationException(
						"MOVETO segments cannot be broken down into parametric equations.");
			} else if (type == PathIterator.SEG_CLOSE) {
				throw new UnsupportedOperationException(
						"CLOSE segments cannot be broken down into parametric equations.");
			}
			if (transform == null)
				xCoeffs = rValue;
			return rValue;
		}

		public float[] getYCoeffs() {
			return getYCoeffs(null);
		}

		public float[] getYCoeffs(AffineTransform transform) {
			if (prev == null)
				throw new NullPointerException("prev was null");
			if (prev.data == null)
				throw new NullPointerException("prev.data was null");
			if (transform != null && transform.isIdentity())
				transform = null;
			if (yCoeffs != null && transform == null)
				return yCoeffs;

			double[] last = new double[] { prev.data[prev.data.length - 2],
					prev.data[prev.data.length - 1] };
			if (transform != null) {
				transform.transform(last, 0, last, 0, 1);
			}
			float[] myData;
			if (transform != null) {
				myData = new float[data.length];
				transform.transform(data, 0, myData, 0, data.length / 2);
			} else {
				myData = data;
			}
			float[] rValue = null;
			if (type == PathIterator.SEG_CUBICTO) {
				rValue = new float[4];
				rValue[0] = (float) (-last[1] + 3 * myData[1] - 3 * myData[3]
						+ myData[5]);
				rValue[1] = (float) (3 * last[1] - 6 * myData[1]
						+ 3 * myData[3]);
				rValue[2] = (float) (-3 * last[1] + 3 * myData[1]);
				rValue[3] = (float) (last[1]);
			} else if (type == PathIterator.SEG_QUADTO) {
				rValue = new float[3];
				rValue[0] = (float) (last[1] - 2 * myData[1] + myData[3]);
				rValue[1] = (float) (-2 * last[1] + 2 * myData[1]);
				rValue[2] = (float) (last[1]);
			} else if (type == PathIterator.SEG_LINETO) {
				rValue = new float[2];
				rValue[0] = (float) (-last[1] + myData[1]);
				rValue[1] = (float) (last[1]);
			} else if (type == PathIterator.SEG_MOVETO) {
				throw new UnsupportedOperationException(
						"MOVETO segments cannot be broken down into parametric equations.");
			} else if (type == PathIterator.SEG_CLOSE) {
				throw new UnsupportedOperationException(
						"CLOSE segments cannot be broken down into parametric equations.");
			}
			if (transform == null)
				yCoeffs = rValue;
			return rValue;
		}

		/**
		 * Returns the tangent angle at the point t.
		 * 
		 * @param t
		 *            the t-value to get the tangent angle for.
		 * @param confine
		 *            this may confine the result to [-pi/2,pi/2]. If this is
		 *            false, then the result may range from [-pi,pi].
		 * @return the tangent angle (in radians).
		 */
		public float getTheta(float t, AffineTransform transform,
				boolean confine) {
			float angle = getTheta(t, transform, 0);
			if (confine) {
				if (angle > Math.PI / 2) {
					angle = angle - (float) Math.PI;
				} else if (angle < -Math.PI / 2) {
					angle = angle + (float) Math.PI;
				}
			}
			return angle;
		}

		public boolean isThetaWellDefined(float t) {
			float[] x_coeffs = getXCoeffs();
			float[] y_coeffs = getYCoeffs();
			float dx, dy;
			if (x_coeffs.length == 2) {
				dx = x_coeffs[0];
				dy = y_coeffs[0];
			} else if (x_coeffs.length == 3) {
				dx = 2 * x_coeffs[0] * t + x_coeffs[1];
				dy = 2 * y_coeffs[0] * t + y_coeffs[1];
			} else if (x_coeffs.length == 4) {
				dx = 3 * x_coeffs[0] * t * t + 2 * x_coeffs[1] * t
						+ x_coeffs[2];
				dy = 3 * y_coeffs[0] * t * t + 2 * y_coeffs[1] * t
						+ y_coeffs[2];
			} else {
				System.err.println("x_coeffs.length = " + x_coeffs.length);
				System.err.println(this);
				throw new RuntimeException("Unexpected condition.");
			}
			if (Math.abs(dx) < ZERO && Math.abs(dy) < ZERO)
				return false;
			return true;
		}

		/**
		 * Returns the tangent angle at the point t.
		 * 
		 * @param t
		 *            the t-value to get the tangent angle for.
		 * @param direction
		 *            in the (extremely) rare case where the tangent slope is
		 *            ambiguous, this indicates which side of the t argument to
		 *            look at. That is, if you approach t from a smaller value,
		 *            the angle might be K. If you approach t from a larger
		 *            value, the angle might be (K+pi). This will happen when
		 *            the direction of the curve is undefined at t, because the
		 *            curve is switching from K to (K+pi). When this argument is
		 *            positive, it assumes t is being approached by values
		 *            greater than t. When negative, it assumes t is approached
		 *            from values less than t.
		 * @return the tangent angle (in radians).
		 */
		public float getTheta(float t, AffineTransform transform,
				int direction) {
			float[] x_coeffs = getXCoeffs(transform);
			float[] y_coeffs = getYCoeffs(transform);
			float dx, dy;
			if (x_coeffs.length == 2) {
				dx = x_coeffs[0];
				dy = y_coeffs[0];
			} else if (x_coeffs.length == 3) {
				dx = 2 * x_coeffs[0] * t + x_coeffs[1];
				dy = 2 * y_coeffs[0] * t + y_coeffs[1];
			} else if (x_coeffs.length == 4) {
				dx = 3 * x_coeffs[0] * t * t + 2 * x_coeffs[1] * t
						+ x_coeffs[2];
				dy = 3 * y_coeffs[0] * t * t + 2 * y_coeffs[1] * t
						+ y_coeffs[2];
			} else {
				System.err.println("x_coeffs.length = " + x_coeffs.length);
				System.err.println(this);
				throw new RuntimeException("Unexpected condition.");
			}

			if (!(Math.abs(dx) < ZERO && Math.abs(dy) < ZERO)) {
				float angle = (float) Math.atan2(dy, dx);
				return angle;
			}

			// If both dx and dy approach zero, a friend who's much smarter
			// than I am pointed out L'Hopital's rule says you can then
			// take the next derivative
			if (x_coeffs.length == 3) {
				dx = 2 * x_coeffs[0];
				dy = 2 * y_coeffs[0];
			} else if (x_coeffs.length == 4) {
				dx = 6 * x_coeffs[0] * t + 2 * x_coeffs[1];
				dy = 6 * y_coeffs[0] * t + 2 * y_coeffs[1];
			}

			float angle1 = (float) Math.atan2(dy, dx);
			float angle2;
			if (angle1 > 0) {
				angle2 = angle1 - (float) Math.PI;
			} else {
				angle2 = angle1 + (float) Math.PI;
			}

			if (direction == 0)
				return angle1;
			if (x_coeffs.length == 2)
				return angle1; // you could throw an exception here, too?

			dx = 0;
			dy = 0;
			if (direction > 1)
				direction = 1;
			if (direction < 1)
				direction = -1;
			float incr = .000001f * direction;

			while (Math.abs(dx) < ZERO && Math.abs(dy) < ZERO) {
				if (x_coeffs.length == 3) {
					dx = 2 * x_coeffs[0] * t + x_coeffs[1];
					dy = 2 * y_coeffs[0] * t + y_coeffs[1];
				} else if (x_coeffs.length == 4) {
					dx = 3 * x_coeffs[0] * t * t + 2 * x_coeffs[1] * t
							+ x_coeffs[2];
					dy = 3 * y_coeffs[0] * t * t + 2 * y_coeffs[1] * t
							+ y_coeffs[2];
				}
				t = t + incr;
			}

			float otherAngle = (float) Math.atan2(dy, dx);
			if (difference(otherAngle, angle1) < difference(otherAngle,
					angle2)) {
				return angle1;
			}
			return angle2;
		}

		private float difference(float angle1, float angle2) {
			float diff = Math.abs(angle1 - angle2);
			if (diff > Math.PI) {
				diff = (float) (2 * Math.PI - diff);
			}
			return diff;
		}

		public float getX(float t) {
			float[] x_coeffs = getXCoeffs();
			if (x_coeffs.length == 2) {
				return x_coeffs[0] * t + x_coeffs[1];
			} else if (x_coeffs.length == 3) {
				return x_coeffs[0] * t * t + x_coeffs[1] * t + x_coeffs[2];
			} else if (x_coeffs.length == 4) {
				return x_coeffs[0] * t * t * t + x_coeffs[1] * t * t
						+ x_coeffs[2] * t + x_coeffs[3];
			} else {
				System.err.println("x_coeffs.length = " + x_coeffs.length);
				System.err.println(this);
				throw new RuntimeException("Unexpected condition.");
			}
		}

		public float getY(float t) {
			float[] y_coeffs = getYCoeffs();
			if (y_coeffs.length == 2) {
				return y_coeffs[0] * t + y_coeffs[1];
			} else if (y_coeffs.length == 3) {
				return y_coeffs[0] * t * t + y_coeffs[1] * t + y_coeffs[2];
			} else if (y_coeffs.length == 4) {
				return y_coeffs[0] * t * t * t + y_coeffs[1] * t * t
						+ y_coeffs[2] * t + y_coeffs[3];
			} else {
				System.err.println("y_coeffs.length = " + y_coeffs.length);
				System.err.println(this);
				throw new RuntimeException("Unexpected condition.");
			}
		}

		@Override
		public String toString() {
			return toString((Float) null);
		}

		public String toString(Float end) {
			return "PathSegment.Float[ " + getPath(end) + " ]";
		}

		protected String getPath(Float end) {
			StringBuffer sb = new StringBuffer();
			Float f = this;
			while (f != end && f != null) {
				if (f.type == PathIterator.SEG_MOVETO) {
					sb.append("m " + f.data[0] + " " + f.data[1] + " ");
				} else if (f.type == PathIterator.SEG_LINETO) {
					sb.append("l " + f.data[0] + " " + f.data[1] + " ");
				} else if (f.type == PathIterator.SEG_QUADTO) {
					sb.append("q " + f.data[0] + " " + f.data[1] + " "
							+ f.data[2] + " " + f.data[3] + " ");
				} else if (f.type == PathIterator.SEG_CUBICTO) {
					sb.append("c " + f.data[0] + " " + f.data[1] + " "
							+ f.data[2] + " " + f.data[3] + " " + f.data[4]
							+ " " + f.data[5] + " ");
				} else if (f.type == PathIterator.SEG_CLOSE) {
					sb.append("z ");
				} else {
					throw new RuntimeException("Unexpected type: " + type);
				}
				f = f.next;
			}
			return sb.toString().trim();
		}

		public Float moveTo(float x, float y) {
			Float newSegment = newSegment();
			newSegment.type = PathIterator.SEG_MOVETO;
			newSegment.data = new float[] { x, y };
			append(newSegment);
			return newSegment;
		}

		public Float lineTo(float x, float y) {
			Float newSegment = newSegment();
			newSegment.type = PathIterator.SEG_LINETO;
			newSegment.data = new float[] { x, y };
			append(newSegment);
			return newSegment;
		}

		public Float quadTo(float cx, float cy, float x, float y) {
			Float newSegment = newSegment();
			newSegment.type = PathIterator.SEG_QUADTO;
			newSegment.data = new float[] { cx, cy, x, y };
			append(newSegment);
			return newSegment;
		}

		public Float cubicTo(float cx0, float cy0, float cx1, float cy1,
				float x1, float y1) {
			Float newSegment = newSegment();
			newSegment.type = PathIterator.SEG_CUBICTO;
			newSegment.data = new float[] { cx0, cy0, cx1, cy1, x1, y1 };
			append(newSegment);
			return newSegment;
		}

		public Float close() {
			Float newSegment = newSegment();
			newSegment.type = PathIterator.SEG_CLOSE;
			append(newSegment);
			return newSegment;
		}

		protected void append(Float seg) {
			if (next != null)
				throw new RuntimeException(
						"Illegal attempt to append shape data to a segment that already has a next segment.");
			if (seg.prev != null)
				throw new RuntimeException(
						"Illegal attempt to append shape data that already exists in another sequence.");
			next = seg;
			seg.prev = this;
		}

		public Float getHead() {
			Float t = this;
			while (t.prev != null) {
				t = t.prev;
			}
			return t;
		}

		public Float getTail() {
			Float t = this;
			while (t.next != null) {
				t = t.next;
			}
			return t;
		}

		@Override
		public void rotate(float f) {
			if (f == 0)
				return;

			float cos = (float) Math.cos(f);
			float sin = (float) Math.sin(f);
			for (int a = 0; a < data.length; a += 2) {
				float x = data[a];
				float y = data[a + 1];
				data[a] = cos * x - sin * y;
				data[a + 1] = sin * x + cos * y;
			}
			xCoeffs = null;
			yCoeffs = null;
		}

		public void write(PathWriter dest, float t0, float t1,
				AffineTransform transform) {
			if (type == PathIterator.SEG_LINETO) {
				double[] pt = new double[] { getX(t1), getY(t1) };
				if (transform != null) {
					transform.transform(pt, 0, pt, 0, 1);
				}
				dest.lineTo((float) pt[0], (float) pt[1]);
			} else if (type == PathIterator.SEG_QUADTO) {
				float[] x_ = getXCoeffs(transform);
				float[] y_ = getYCoeffs(transform);
				PathWriter.quadTo(dest, t0, t1, x_[0], x_[1], x_[2], y_[0],
						y_[1], y_[2]);
			} else if (type == PathIterator.SEG_CUBICTO) {
				float[] x_ = getXCoeffs(transform);
				float[] y_ = getYCoeffs(transform);
				PathWriter.cubicTo(dest, t0, t1, x_[0], x_[1], x_[2], x_[3],
						y_[0], y_[1], y_[2], y_[3]);
			} else if (type == PathIterator.SEG_MOVETO) {
				double[] pt = new double[] { data[0], data[1] };
				if (transform != null) {
					transform.transform(pt, 0, pt, 0, 1);
				}
				dest.moveTo((float) pt[0], (float) pt[1]);
			} else {
				throw new UnsupportedOperationException(
						toTypeName(type) + " not supported here.");
			}
		}

		public Point2D[] getPoints(boolean includePrecedingEnd) {
			List<Point2D> returnValue = new ArrayList<>(4);
			if (includePrecedingEnd) {
				if (prev.type == PathIterator.SEG_MOVETO
						|| prev.type == PathIterator.SEG_LINETO) {
					returnValue
							.add(new Point2D.Float(prev.data[0], prev.data[1]));
				} else if (prev.type == PathIterator.SEG_QUADTO) {
					returnValue
							.add(new Point2D.Float(prev.data[2], prev.data[3]));
				} else if (prev.type == PathIterator.SEG_CUBICTO) {
					returnValue
							.add(new Point2D.Float(prev.data[4], prev.data[5]));
				} else if (prev.type == PathIterator.SEG_CLOSE) {
					// skip
				} else {
					throw new RuntimeException(
							"unsupported type: " + prev.type);
				}
			}

			if (type == PathIterator.SEG_MOVETO
					|| type == PathIterator.SEG_LINETO) {
				returnValue.add(new Point2D.Float(data[0], data[1]));
			} else if (type == PathIterator.SEG_QUADTO) {
				returnValue.add(new Point2D.Float(data[0], data[1]));
				returnValue.add(new Point2D.Float(data[2], data[3]));
			} else if (type == PathIterator.SEG_CUBICTO) {
				returnValue.add(new Point2D.Float(data[0], data[1]));
				returnValue.add(new Point2D.Float(data[2], data[3]));
				returnValue.add(new Point2D.Float(data[4], data[5]));
			} else if (type == PathIterator.SEG_CLOSE) {
				Float t = this;
				while (t.type != PathIterator.SEG_MOVETO) {
					t = t.prev;
				}
				returnValue.add(new Point2D.Float(t.data[0], t.data[1]));
			} else {
				throw new RuntimeException("unsupported type: " + prev.type);
			}
			return returnValue.toArray(new Point2D[0]);
		}
	}

	public static String toString(float[] array) {
		if (array == null)
			return null;
		StringBuffer sb = new StringBuffer();
		sb.append("[");
		for (int a = 0; a < array.length; a++) {
			if (a != 0) {
				sb.append(", ");
			}
			sb.append(java.lang.Float.toString(array[a]));
		}
		sb.append("]");
		return sb.toString();
	}
}