/**
 * 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.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;

/**
 * This object writes path data.
 * <P>
 * With this abstract class you could filter shape data as it is being written.
 * The simplest extension of this is the {@link GeneralPathWriter}.
 * 
 * <P>
 * This is designed to match the <code>GeneralPath</code> method signatures.
 * 
 */
public abstract class PathWriter {

	/**
	 * Adds a point to the path by moving to the specified coordinates.
	 * 
	 * @param x
	 *            the x-coordinate to move to
	 * @param y
	 *            the y-coordinate to move to
	 */
	public abstract void moveTo(float x, float y);

	/**
	 * Adds a point to the path by drawing a straight line from the current
	 * coordinates to the new specified coordinates.
	 *
	 * @param x
	 *            the x-coordinate of the end point.
	 * @param y
	 *            the y-coordinate of the end point.
	 */
	public abstract void lineTo(float x, float y);

	/**
	 * Adds a curved segment, defined by two new points, to the path by drawing
	 * a Quadratic curve that intersects both the current coordinates and the
	 * coordinates (x2, y2), using the specified point (x1, y1) as a quadratic
	 * parametric control point.
	 *
	 * @param cx
	 *            the x-coordinate of the control point.
	 * @param cy
	 *            the y-coordinate of the control point.
	 * @param x
	 *            the x-coordinate of the end point.
	 * @param y
	 *            the y-coordinate of the end point.
	 */
	public abstract void quadTo(float cx, float cy, float x, float y);

	/**
	 * Adds a curved segment, defined by three new points, to the path by
	 * drawing a Bezier curve that intersects both the current coordinates and
	 * the coordinates (x3, y3), using the specified points (x1, y1) and (x2,
	 * y2) as Bezier control points.
	 * 
	 * @param cx1
	 *            the x-coordinate of the first control point.
	 * @param cy1
	 *            the y-coordinate of the first control point.
	 * @param cx2
	 *            the x-coordinate of the second control point.
	 * @param cy2
	 *            the y-coordinate of the second control point.
	 * @param x
	 *            the x-coordinate of the end point.
	 * @param y
	 *            the y-coordinate of the end point.
	 * */
	public abstract void curveTo(float cx1, float cy1, float cx2, float cy2,
			float x, float y);

	/**
	 * Closes the current subpath by drawing a straight line back to the
	 * coordinates of the last moveTo.
	 */
	public abstract void closePath();

	/**
	 * This guarantees that this writer has flushed all currently written
	 * information.
	 * <P>
	 * For example, some writers may need to perform complex calculations and
	 * re-organize shapes. Or some writers may simply be buffering path
	 * instructions to better manage memory. Calling this method guarantees that
	 * all path instructions be immediately processed.
	 * <P>
	 * Because some writers may be manipulating path data you should <i>not call
	 * this method</i> until all shape data has been written.
	 * 
	 */
	public abstract void flush();

	/**
	 * Writes a shape.
	 * <P>
	 * This iterates through the shape and makes the appropriate calls to
	 * <code>moveTo()</code>, <code>curveTo()</code>, etc.
	 * 
	 * @param s
	 *            the shape to write.
	 */
	public void write(Shape s) {
		write(s.getPathIterator(null));
	}

	/**
	 * Writes a path.
	 * <P>
	 * This iterates through the path and makes the appropriate calls to
	 * <code>moveTo()</code>, <code>curveTo()</code>, etc.
	 * 
	 * @param i
	 *            the path to write.
	 */
	public void write(PathIterator i) {
		float[] coords = new float[6];
		int k;
		while (i.isDone() == false) {
			k = i.currentSegment(coords);
			if (k == PathIterator.SEG_MOVETO) {
				moveTo(coords[0], coords[1]);
			} else if (k == PathIterator.SEG_LINETO) {
				lineTo(coords[0], coords[1]);
			} else if (k == PathIterator.SEG_QUADTO) {
				quadTo(coords[0], coords[1], coords[2], coords[3]);
			} else if (k == PathIterator.SEG_CUBICTO) {
				curveTo(coords[0], coords[1], coords[2], coords[3], coords[4],
						coords[5]);
			} else if (k == PathIterator.SEG_CLOSE) {
				closePath();
			} else {
				throw new RuntimeException("Unexpected segment: " + k);
			}
			i.next();
		}
	}

	/**
	 * This appends a cubic curve to the GeneralPath provided that ranges from t
	 * = t0 to t = t1. This assumes that the path already ends at the point
	 * (ax*t0*t0*t0+bx*t0*t0+cx*t0+dx, ay*t0*t0*t0+by*t0*t0+cy*t0+dx).
	 * 
	 * @param path
	 *            the path to append data to.
	 * @param t0
	 *            the t-value for the beginning of this curve
	 * @param t1
	 *            the t-value for the end of this curve
	 * @param ax
	 *            the coefficient for the (t^3) term of the x parametric
	 *            equation
	 * @param bx
	 *            the coefficient for the (t^2) term of the x parametric
	 *            equation
	 * @param cx
	 *            the coefficient for the t term of the x parametric equation
	 * @param dx
	 *            the constant for the x parametric equation.
	 * @param ay
	 *            the coefficient for the (t^3) term of the y parametric
	 *            equation
	 * @param by
	 *            the coefficient for the (t^2) term of the y parametric
	 *            equation
	 * @param cy
	 *            the coefficient for the t term of the y parametric equation
	 * @param dy
	 *            the constant for the y parametric equation.
	 */
	public static void cubicTo(GeneralPath path, double t0, double t1,
			double ax, double bx, double cx, double dx, double ay, double by,
			double cy, double dy) {
		cubicTo2(path, t0, t1, ax, bx, cx, dx, ay, by, cy, dy);
	}

	/**
	 * This appends a cubic curve to the PathWriter provided that ranges from t
	 * = t0 to t = t1. This assumes that the path already ends at the point
	 * (ax*t0*t0*t0+bx*t0*t0+cx*t0+dx, ay*t0*t0*t0+by*t0*t0+cy*t0+dx).
	 * 
	 * @param path
	 *            the path to append data to.
	 * @param t0
	 *            the t-value for the beginning of this curve
	 * @param t1
	 *            the t-value for the end of this curve
	 * @param ax
	 *            the coefficient for the (t^3) term of the x parametric
	 *            equation
	 * @param bx
	 *            the coefficient for the (t^2) term of the x parametric
	 *            equation
	 * @param cx
	 *            the coefficient for the t term of the x parametric equation
	 * @param dx
	 *            the constant for the x parametric equation.
	 * @param ay
	 *            the coefficient for the (t^3) term of the y parametric
	 *            equation
	 * @param by
	 *            the coefficient for the (t^2) term of the y parametric
	 *            equation
	 * @param cy
	 *            the coefficient for the t term of the y parametric equation
	 * @param dy
	 *            the constant for the y parametric equation.
	 */
	public static void cubicTo(PathWriter path, double t0, double t1,
			double ax, double bx, double cx, double dx, double ay, double by,
			double cy, double dy) {
		cubicTo2(path, t0, t1, ax, bx, cx, dx, ay, by, cy, dy);
	}

	private static void cubicTo2(Object obj, double t0, double t1, double ax,
			double bx, double cx, double dx, double ay, double by, double cy,
			double dy) {
		double tW = 2.0 * t0 / 3.0 + t1 / 3.0;
		double tZ = t0 / 3.0 + 2.0 * t1 / 3.0;

		double f0 = ay * t0 * t0 * t0 + by * t0 * t0 + cy * t0 + dy;
		double f1 = ay * tW * tW * tW + by * tW * tW + cy * tW + dy;
		double f2 = ay * tZ * tZ * tZ + by * tZ * tZ + cy * tZ + dy;
		double f3 = ay * t1 * t1 * t1 + by * t1 * t1 + cy * t1 + dy;

		double dy2 = f0;
		double cy2 = (-11 * f0 + 18 * f1 - 9 * f2 + 2 * f3) / 2.0;
		double by2 = (-19 * f0 + 27 * f2 - 8 * f3 - 10 * cy2) / 4;
		double ay2 = f3 - by2 - cy2 - f0;

		f0 = ax * t0 * t0 * t0 + bx * t0 * t0 + cx * t0 + dx;
		f1 = ax * tW * tW * tW + bx * tW * tW + cx * tW + dx;
		f2 = ax * tZ * tZ * tZ + bx * tZ * tZ + cx * tZ + dx;
		f3 = ax * t1 * t1 * t1 + bx * t1 * t1 + cx * t1 + dx;

		double dx2 = f0;
		double cx2 = (-11 * f0 + 18 * f1 - 9 * f2 + 2 * f3) / 2.0;
		double bx2 = (-19 * f0 + 27 * f2 - 8 * f3 - 10 * cx2) / 4;
		double ax2 = f3 - bx2 - cx2 - f0;

		double cy0 = (3 * dy2 + cy2) / 3;
		double cy1 = (by2 - 3 * dy2 + 6 * cy0) / 3;
		double y1 = ay2 + dy2 - 3 * cy0 + 3 * cy1;

		double cx0 = (3 * dx2 + cx2) / 3;
		double cx1 = (bx2 - 3 * dx2 + 6 * cx0) / 3;
		double x1 = ax2 + dx2 - 3 * cx0 + 3 * cx1;

		if (obj instanceof GeneralPath) {
			((GeneralPath) obj).curveTo((float) cx0, (float) cy0, (float) cx1,
					(float) cy1, (float) x1, (float) y1);
		} else if (obj instanceof PathWriter) {
			((PathWriter) obj).curveTo((float) cx0, (float) cy0, (float) cx1,
					(float) cy1, (float) x1, (float) y1);
		}
	}

	/**
	 * This appends a quadratic curve to the GeneralPath provided that ranges
	 * from t = t0 to t = t1. This assumes that the path already ends at the
	 * point (ax*t0*t0+bx*t0+cx, ay*t0*t0+by*t0+cy).
	 * 
	 * @param path
	 *            the path to append data to.
	 * @param t0
	 *            the t-value for the beginning of this curve
	 * @param t1
	 *            the t-value for the end of this curve
	 * @param ax
	 *            the coefficient for the (t^2) term of the x parametric
	 *            equation
	 * @param bx
	 *            the coefficient for the t term of the x parametric equation
	 * @param cx
	 *            the constant for the x parametric equation.
	 * @param ay
	 *            the coefficient for the (t^2) term of the y parametric
	 *            equation
	 * @param by
	 *            the coefficient for the t term of the y parametric equation
	 * @param cy
	 *            the constant for the y parametric equation.
	 */
	public static void quadTo(GeneralPath path, double t0, double t1,
			double ax, double bx, double cx, double ay, double by, double cy) {
		quadTo2(path, t0, t1, ax, bx, cx, ay, by, cy);
	}

	/**
	 * This appends a quadratic curve to the PathWriter provided that ranges
	 * from t = t0 to t = t1. This assumes that the path already ends at the
	 * point (ax*t0*t0+bx*t0+cx, ay*t0*t0+by*t0+cy).
	 * 
	 * @param path
	 *            the path to append data to.
	 * @param t0
	 *            the t-value for the beginning of this curve
	 * @param t1
	 *            the t-value for the end of this curve
	 * @param ax
	 *            the coefficient for the (t^2) term of the x parametric
	 *            equation
	 * @param bx
	 *            the coefficient for the t term of the x parametric equation
	 * @param cx
	 *            the constant for the x parametric equation.
	 * @param ay
	 *            the coefficient for the (t^2) term of the y parametric
	 *            equation
	 * @param by
	 *            the coefficient for the t term of the y parametric equation
	 * @param cy
	 *            the constant for the y parametric equation.
	 */
	public static void quadTo(PathWriter path, double t0, double t1, double ax,
			double bx, double cx, double ay, double by, double cy) {
		quadTo2(path, t0, t1, ax, bx, cx, ay, by, cy);
	}

	private static void quadTo2(Object obj, double t0, double t1, double ax,
			double bx, double cx, double ay, double by, double cy) {
		double tZ = (t0 + t1) / 2.0;

		double f0 = ay * t0 * t0 + by * t0 + cy;
		double f1 = ay * tZ * tZ + by * tZ + cy;
		double f2 = ay * t1 * t1 + by * t1 + cy;

		double ay2 = 2 * f2 - 4 * f1 + 2 * f0;
		double cy2 = f0;
		double by2 = f2 - cy2 - ay2;

		f0 = ax * t0 * t0 + bx * t0 + cx;
		f1 = ax * tZ * tZ + bx * tZ + cx;
		f2 = ax * t1 * t1 + bx * t1 + cx;

		double ax2 = 2 * f2 - 4 * f1 + 2 * f0;
		double cx2 = f0;
		double bx2 = f2 - cx2 - ax2;

		double ctrlY = (2 * cy2 + by2) / 2;
		double y1 = ay2 - cy2 + 2 * ctrlY;

		double ctrlX = (2 * cx2 + bx2) / 2;
		double x1 = ax2 - cx2 + 2 * ctrlX;

		if (obj instanceof GeneralPath) {
			((GeneralPath) obj).quadTo((float) ctrlX, (float) ctrlY,
					(float) x1, (float) y1);
		} else if (obj instanceof PathWriter) {
			((PathWriter) obj).quadTo((float) ctrlX, (float) ctrlY, (float) x1,
					(float) y1);
		}
	}
}