/**
 * 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.awt;

import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;

import com.pump.geom.GeneralPathWriter;
import com.pump.geom.PathSegment;
import com.pump.geom.PathWriter;
import com.pump.geom.SimplifiedPathIterator;

/**
 * This creates a calligraphic or ribbon-like effect while tracing a shape.
 * <p>
 * There are two separate shape bodies being written. If you paint them
 * separately: this creates an "over" and "under" effect. If you paint them the
 * same color: then this class is basically a <code>java.awt.Stroke</code>.
 */
public class CalligraphyPathWriter extends PathWriter {
	private final float angle, offset1, offset2;
	private final PathWriter dest1, dest2;

	/**
	 * null=undefined, true=dest1, false=dest2
	 */
	private Boolean writingTrack1;
	private PathSegment.Float tail;
	private float moveX, moveY;
	private final AffineTransform rotate;

	/**
	 * This is only used internally when we require a PathWriter to write
	 * fragments of paths.
	 */
	PathWriter tailWriter = new PathWriter() {
		@Override
		public void moveTo(float x, float y) {
			throw new UnsupportedOperationException("moveTo(" + x + ", " + y
					+ ")");
		}

		@Override
		public void lineTo(float x, float y) {
			tail = tail.lineTo(x, y);
		}

		@Override
		public void quadTo(float cx, float cy, float x, float y) {
			tail = tail.quadTo(cx, cy, x, y);
		}

		@Override
		public void curveTo(float cx1, float cy1, float cx2, float cy2,
				float x, float y) {
			tail = tail.cubicTo(cx1, cy1, cx2, cy2, x, y);
		}

		@Override
		public void closePath() {
			throw new UnsupportedOperationException("closePath()");
		}

		@Override
		public void flush() {
		}
	};

	/**
	 * Create a new <code>CalligraphyPathWriter</code>.
	 * 
	 * @param angle
	 *            the angle of the nib in this path.
	 * @param offset1
	 *            the offset of one side of the traced shape. One side of shape
	 *            you trace will be offset by: <br>
	 *            (+offset1*cos(angle), +offset1*sin(angle) )
	 *            <p>
	 *            It is possible for this value to be zero, which means one side
	 *            of this path will exactly line up with the original shape
	 *            being traced.
	 *            <p>
	 *            If both offsets are equal: then no new path segments will be
	 *            visible because they will be pencil then.
	 *            <p>
	 *            To achieve a simple stroke: one offset should be (+width/2),
	 *            and the other offset should be (-width/2).
	 * @param offset2
	 *            the offset of the other side of the traced shape. This side of
	 *            shape will be offset by: <br>
	 *            (+offset2*cos(angle), +offset2*sin(angle) )
	 * @param destination
	 *            the destination for the data to write.
	 */
	public CalligraphyPathWriter(float angle, float offset1, float offset2,
			GeneralPath destination) {
		this(angle, offset1, offset2, new GeneralPathWriter(destination),
				new GeneralPathWriter(destination));
	}

	/**
	 * Create a new <code>CalligraphyPathWriter</code>.
	 * 
	 * @param angle
	 *            the angle of the nib in this path.
	 * @param offset1
	 *            the offset of one side of the traced shape. One side of shape
	 *            you trace will be offset by: <br>
	 *            (+offset1*cos(angle), +offset1*sin(angle) )
	 *            <p>
	 *            It is possible for this value to be zero, which means one side
	 *            of this path will exactly line up with the original shape
	 *            being traced.
	 *            <p>
	 *            If both offsets are equal: then no new path segments will be
	 *            visible because they will be pencil then.
	 *            <p>
	 *            To achieve a simple stroke: one offset should be (+width/2),
	 *            and the other offset should be (-width/2).
	 * @param offset2
	 *            the offset of the other side of the traced shape. This side of
	 *            shape will be offset by: <br>
	 *            (+offset2*cos(angle), +offset2*sin(angle) )
	 * @param dest1
	 *            the destination for half of the calligraphic shape.
	 * @param dest2
	 *            the destination for the other half. This may be the same as
	 *            <code>dest1</code> if you plan on painting this path at the
	 *            same time with the same fill.
	 */
	public CalligraphyPathWriter(float angle, float offset1, float offset2,
			PathWriter dest1, PathWriter dest2) {
		checkArgument(angle, "angle is invalid");
		checkArgument(offset1, "offset1 is invalid");
		checkArgument(offset2, "offset2 is invalid");
		if (dest1 == null && dest2 == null)
			throw new NullPointerException();
		this.angle = angle;
		this.offset1 = offset1;
		this.offset2 = offset2;
		this.dest1 = dest1;
		this.dest2 = dest2;
		rotate = AffineTransform.getRotateInstance(-angle);
	}

	private void checkArgument(float value, String text) {
		if (Float.isInfinite(value) || Float.isNaN(value))
			throw new IllegalArgumentException(text + " (" + value + ")");
	}

	@Override
	public synchronized void moveTo(float x, float y) {
		checkArgument(x, "x is invalid");
		checkArgument(y, "y is invalid");
		flush(x, y);
		moveX = x;
		moveY = y;
	}

	@Override
	public synchronized void lineTo(final float x, final float y) {
		checkArgument(x, "x is invalid");
		checkArgument(y, "y is invalid");
		if (tail == null)
			throw new NullPointerException("missing moveTo segment");

		PathSegment.Float moveTo = new PathSegment.Float(
				tail.data[tail.data.length - 2],
				tail.data[tail.data.length - 1]);
		PathSegment.Float line = moveTo.lineTo(x, y);

		defineTrack(0, 1, line);

		tail = tail.lineTo(x, y);
	}

	@Override
	public synchronized void quadTo(final float cx, final float cy,
			final float x, final float y) {
		checkArgument(cx, "cx is invalid");
		checkArgument(cy, "cy is invalid");
		checkArgument(x, "x is invalid");
		checkArgument(y, "y is invalid");
		if (tail == null)
			throw new NullPointerException("missing moveTo segment");

		PathSegment.Float moveTo = new PathSegment.Float(
				tail.data[tail.data.length - 2],
				tail.data[tail.data.length - 1]);
		PathSegment.Float quad = moveTo.quadTo(cx, cy, x, y);

		// first, check to see if this is really quad data:
		int simplifiedSegment = SimplifiedPathIterator.simplify(
				PathIterator.SEG_QUADTO, moveTo.data[0], moveTo.data[1],
				quad.data);
		if (simplifiedSegment == PathIterator.SEG_LINETO) {
			lineTo(quad.data[0], quad.data[1]);
			return;
		}

		float[] ry_ = quad.getYCoeffs(rotate);

		float t = -ry_[1] / (2 * ry_[0]);
		if (t >= 0 && t <= 1) {
			float[] x_ = quad.getXCoeffs();
			float[] y_ = quad.getYCoeffs();

			defineTrack(0, t, quad);
			PathWriter.quadTo(tailWriter, 0, t, x_[0], x_[1], x_[2], y_[0],
					y_[1], y_[2]);
			flush(quad.getX(t), quad.getY(t));

			defineTrack(t, 1, quad);
			PathWriter.quadTo(tailWriter, t, 1, x_[0], x_[1], x_[2], y_[0],
					y_[1], y_[2]);
		} else {
			defineTrack(0, 1, quad);
			tail = tail.quadTo(cx, cy, x, y);
		}

	}

	@Override
	public synchronized void curveTo(float cx1, float cy1, float cx2,
			float cy2, float x, float y) {
		checkArgument(cx1, "cx1 is invalid");
		checkArgument(cy1, "cy1 is invalid");
		checkArgument(cx2, "cx2 is invalid");
		checkArgument(cy2, "cy2 is invalid");
		checkArgument(x, "x is invalid");
		checkArgument(y, "y is invalid");
		if (tail == null)
			throw new NullPointerException("missing moveTo segment");

		PathSegment.Float moveTo = new PathSegment.Float(
				tail.data[tail.data.length - 2],
				tail.data[tail.data.length - 1]);
		PathSegment.Float cubic = moveTo.cubicTo(cx1, cy1, cx2, cy2, x, y);

		// first, check to see if this is really cubic data:
		int simplifiedSegment = SimplifiedPathIterator.simplify(
				PathIterator.SEG_CUBICTO, moveTo.data[0], moveTo.data[1],
				cubic.data);
		if (simplifiedSegment == PathIterator.SEG_LINETO) {
			lineTo(cubic.data[0], cubic.data[1]);
			return;
		} else if (simplifiedSegment == PathIterator.SEG_QUADTO) {
			quadTo(cubic.data[0], cubic.data[1], cubic.data[2], cubic.data[3]);
			return;
		}

		float[] ry_ = cubic.getYCoeffs(rotate);
		float[] times = null;
		if (Math.abs(ry_[0]) < .00001) {
			// the leading coefficent is effectively 0, so
			// instead of ay*(t^3)+by*(t^2)+cy*t+dy we have
			// 0+bx*(t^2)+cx*t+dx

			float t = -ry_[2] / (2 * ry_[1]);
			if (t >= 0 && t <= 1) {
				times = new float[] { t };
			}
		} else {
			float determinant = 4 * ry_[1] * ry_[1] - 12 * ry_[0] * ry_[2];
			if (determinant >= 0) {
				float t1 = -1;
				float t2 = -1;
				determinant = (float) Math.sqrt(determinant);
				if (determinant == 0) {
					t1 = (-2 * ry_[1]) / (6 * ry_[0]);
				} else {
					t1 = (-2 * ry_[1] + determinant) / (6 * ry_[0]);
					t2 = (-2 * ry_[1] - determinant) / (6 * ry_[0]);
				}

				if (cubic.isThetaWellDefined(t1) == false) {
					t1 = -1;
				}
				if (cubic.isThetaWellDefined(t2) == false) {
					t2 = -1;
				}

				// sort our list of 2 elements:
				if (t2 < t1) {
					float swap = t1;
					t1 = t2;
					t2 = swap;
				}

				if (t1 >= 0 && t2 <= 1) {
					times = new float[] { t1, t2 };
				} else if (t1 >= 0 && t1 <= 1) {
					times = new float[] { t1 };
				} else if (t2 >= 0 && t2 <= 1) {
					times = new float[] { t2 };
				}
			}
		}

		if (times != null) {
			float[] x_ = cubic.getXCoeffs();
			float[] y_ = cubic.getYCoeffs();
			if (times.length == 2) {
				defineTrack(0, times[0], cubic);
				PathWriter.cubicTo(tailWriter, 0, times[0], x_[0], x_[1],
						x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]);
				flush(cubic.getX(times[0]), cubic.getY(times[0]));
				defineTrack(times[0], times[1], cubic);
				PathWriter.cubicTo(tailWriter, times[0], times[1], x_[0],
						x_[1], x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]);
				flush(cubic.getX(times[1]), cubic.getY(times[1]));
				defineTrack(times[1], 1, cubic);
				PathWriter.cubicTo(tailWriter, times[1], 1, x_[0], x_[1],
						x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]);
			} else {
				defineTrack(0, times[0], cubic);
				PathWriter.cubicTo(tailWriter, 0, times[0], x_[0], x_[1],
						x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]);
				flush(cubic.getX(times[0]), cubic.getY(times[0]));
				defineTrack(times[0], 1, cubic);
				PathWriter.cubicTo(tailWriter, times[0], 1, x_[0], x_[1],
						x_[2], x_[3], y_[0], y_[1], y_[2], y_[3]);
			}
			return;
		}
		defineTrack(0, 1, cubic);
		tail = tail.cubicTo(cx1, cy1, cx2, cy2, x, y);
	}

	private void defineTrack(float t0, float t1, PathSegment.Float segment) {
		float[] y_ = segment.getYCoeffs(rotate);

		float t = (t0 + t1) / 2;
		boolean b;
		if (y_.length == 2) {
			b = (y_[0]) > 0;
		} else if (y_.length == 3) {
			b = (2 * y_[0] * t + y_[1]) > 0;
		} else if (y_.length == 4) {
			b = (3 * y_[0] * t * t + 2 * y_[1] * t + y_[2]) > 0;
		} else {
			throw new RuntimeException("unexpected condition");
		}
		if (writingTrack1 != null && writingTrack1.booleanValue() != b) {
			float x = segment.getX(t0);
			float y = segment.getY(t0);
			flush(x, y);
		}

		if (writingTrack1 == null) {
			writingTrack1 = b ? Boolean.TRUE : Boolean.FALSE;
		}
	}

	@Override
	public synchronized void closePath() {
		if (tail == null)
			throw new NullPointerException("missing moveTo segment");
		float lastX = tail.data[tail.data.length - 2];
		float lastY = tail.data[tail.data.length - 1];
		if (Math.abs(lastX - moveX) > .00001
				|| Math.abs(lastY - moveY) > .00001)
			lineTo(moveX, moveY);

		flush();
	}

	@Override
	public void write(Shape s) {
		PathIterator iter = s.getPathIterator(null);
		write(iter);
	}

	private void flush(float moveX, float moveY) {
		flush();
		tail = new PathSegment.Float(moveX, moveY);
	}

	@Override
	public synchronized void flush() {
		try {
			/**
			 * This occurs if there hasn't been a moveTo
			 */
			if (tail == null)
				return;

			/**
			 * This occurs if a moveTo is followed by a close with no segment
			 * data.
			 */
			if (writingTrack1 == null)
				return;

			PathSegment.Float head = tail.getHead();
			PathWriter dest;
			double cos = Math.cos(angle);
			double sin = Math.sin(angle);
			AffineTransform transform1, transform2;

			/**
			 * Switching the order we apply the transforms fixes winding
			 * problems when dest1 and dest2 are the same path. The same basic
			 * path data is being written either way.
			 */
			if (writingTrack1.booleanValue()) {
				dest = dest1;
				transform1 = AffineTransform.getTranslateInstance(
						offset1 * cos, offset1 * sin);
				transform2 = AffineTransform.getTranslateInstance(
						offset2 * cos, offset2 * sin);
			} else {
				dest = dest2;
				transform1 = AffineTransform.getTranslateInstance(
						offset2 * cos, offset2 * sin);
				transform2 = AffineTransform.getTranslateInstance(
						offset1 * cos, offset1 * sin);
			}

			/** This can happen if only 1 dest path was provided to write to. */
			if (dest == null)
				return;

			PathSegment.Float t = head;
			while (t != null) {
				t.write(dest, 0, 1, transform1);
				t = t.next;
			}

			t = tail;
			double[] pts = new double[t.data.length];
			transform2.transform(t.data, 0, pts, 0, t.data.length / 2);
			dest.lineTo((float) pts[pts.length - 2],
					(float) pts[pts.length - 1]);

			while (t != null) {
				if (t.type == PathIterator.SEG_MOVETO) {
					if (t.prev == null) {
						dest.closePath();
					} else {
						throw new RuntimeException("Unexpected condition.");
					}
				} else {
					t.write(dest, 1, 0, transform2);
				}
				t = t.prev;
			}
			dest.flush();
		} finally {
			tail = null;
			writingTrack1 = null;
		}
	}
}