/**
 * 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.GeneralPath;
import java.awt.geom.Point2D;
import java.util.Random;

import com.pump.geom.GeneralPathWriter;
import com.pump.geom.MeasuredShape;
import com.pump.geom.PathWriter;

/**
 * This applies a charcoal effect to a shape.
 * 
 * This basically takes a shape and applies several "cracks" of varying depth at
 * a fixed angle.
 * 
 * (The implementation is pretty simple, and there are a few interesting code
 * snippets commented out that change how this renders.)
 * 
 */
public class CharcoalEffect {
	public final PathWriter writer;
	public final int seed;
	public final float size;
	public final float angle;
	public final float maxDepth;

	/**
	 * Creates a new <code>CharcoalEffect</code>.
	 * 
	 * @param dest
	 *            the destination to write the new shape to.
	 * @param size
	 *            the size of the cracks. This is float from [0,1], where "0"
	 *            means "no crack depth" and "1" means "high depth". The depth
	 *            is always relative to the <i>possible</i> depth.
	 * @param angle
	 *            the angle of the cracks.
	 * @param randomSeed
	 *            the random seed.
	 */
	public CharcoalEffect(PathWriter dest, float size, float angle,
			int randomSeed) {
		this(dest, size, angle, randomSeed, Float.MAX_VALUE);
	}

	/**
	 * Creates a new <code>CharcoalEffect</code>.
	 * 
	 * @param dest
	 *            the destination to write the new shape to.
	 * @param size
	 *            the size of the cracks. This is float from [0,1], where "0"
	 *            means "no crack depth" and "1" means "high depth". The depth
	 *            is always relative to the <i>possible</i> depth.
	 * @param angle
	 *            the angle of the cracks.
	 * @param randomSeed
	 *            the random seed.
	 * @param maxDepth
	 *            this is the maximum crack depth. If this is zero, then no
	 *            cracks will be added. If this is 5, then cracks will be at
	 *            most 5 pixels. If you aren't sure what to make this value, use
	 *            <code>Float.MAX_VALUE</code>.
	 */
	public CharcoalEffect(PathWriter dest, float size, float angle,
			int randomSeed, float maxDepth) {
		if (size < 0 || size > 1)
			throw new IllegalArgumentException("size (" + size
					+ ") must be between 0 and 1.");
		writer = dest;
		seed = randomSeed;
		this.size = size;
		this.angle = angle;
		this.maxDepth = maxDepth;
	}

	/**
	 * Applies the <code>CharcoalEffect</code> to a shape.
	 * 
	 * @param shape
	 *            the shape to apply the effect to.
	 * @param size
	 *            the size of the cracks. This is float from [0,1], where "0"
	 *            means "no crack depth" and "1" means "high depth". The depth
	 *            is always relative to the <i>possible</i> depth.
	 * @param angle
	 *            the angle of the cracks.
	 * @param randomSeed
	 *            the random seed.
	 * @param maxDepth
	 *            this is the maximum crack depth. If this is zero, then no
	 *            cracks will be added. If this is 5, then cracks will be at
	 *            most 5 pixels. If you aren't sure what to make this value, use
	 *            <code>Float.MAX_VALUE</code>.
	 * @return a new filtered path.
	 */
	public static GeneralPath filter(Shape shape, float size, float angle,
			int randomSeed, float maxDepth) {
		GeneralPath path = new GeneralPath();
		GeneralPathWriter writer = new GeneralPathWriter(path);
		CharcoalEffect effect = new CharcoalEffect(writer, size, angle,
				randomSeed, maxDepth);
		effect.write(shape);
		return path;
	}

	/**
	 * Applies the <code>CharcoalEffect</code> to a shape.
	 * 
	 * @param shape
	 *            the shape to apply the effect to.
	 * @param size
	 *            the size of the cracks. This is float from [0,1], where "0"
	 *            means "no crack depth" and "1" means "high depth". The depth
	 *            is always relative to the <i>possible</i> depth.
	 * @param angle
	 *            the angle of the cracks.
	 * @param randomSeed
	 *            the random seed.
	 * @return a new filtered path.
	 */
	public static GeneralPath filter(Shape shape, float size, float angle,
			int randomSeed) {
		return filter(shape, size, angle, randomSeed, Float.MAX_VALUE);
	}

	/**
	 * Applies this effect to the shape provided.
	 * 
	 * @param s
	 *            the shape to write.
	 */
	public void write(Shape s) {
		Random random = new Random(seed);

		Point2D center = new Point2D.Float();
		Point2D rightSide = new Point2D.Float();

		MeasuredShape[] m = MeasuredShape.getSubpaths(s, .05f);
		subpathIterator: for (int a = 0; a < m.length; a++) {
			float orig = m[a].getOriginalDistance();
			float total = m[a].getClosedDistance();
			float distance = 0;
			float pendingGap = 0;

			writer.moveTo(m[a].getMoveToX(), m[a].getMoveToY());

			while (distance < orig) {
				pendingGap += (.05f + .95f * random.nextFloat()) * 20
						* (.05f + .95f * (1 - .9f * size));

				if (distance + pendingGap >= orig) {
					// we're overflowing:
					float remaining = orig - distance;
					if (remaining > 2) {
						m[a].writeShape(distance / total, remaining / total,
								writer, false);
					} else {
						writer.closePath();
					}
					continue subpathIterator;
				} else if (distance + pendingGap < orig) {
					// see if we can add a crack here:

					m[a].getPoint(distance + pendingGap, center);

					/**
					 * Don't add a crack if this point is completely inside the
					 * guiding shape. And don't trust
					 * shape.contains(x,y,width,height). Although that is in
					 * theory what we want, it doesn't always return correct
					 * results! This test isn't quite the same, but gets us what
					 * we need. And accurately:
					 */
					boolean addCrack = !(s.contains(center.getX() - .5,
							center.getY() - .5)
							&& s.contains(center.getX() + .5,
									center.getY() - .5)
							&& s.contains(center.getX() - .5,
									center.getY() + .5) && s.contains(
							center.getX() + .5, center.getY() + .5));

					if (addCrack) {
						for (int mult = -1; mult <= 1; mult += 2) { // try both
																	// the
																	// clockwise
																	// and the
																	// counterclockwise
																	// side
							float width = .05f;
							// yes, you can also try this:
							// float angle =
							// m[a].getTangentSlope(distance+pendingGap);
							// or
							// float angle =
							// m[a].getTangentSlope(distance+pendingGap)-(float)Math.PI/4;
							while (s.contains(
									center.getX()
											+ width
											* Math.cos(angle + mult * Math.PI
													/ 2),
									center.getY()
											+ width
											* Math.sin(angle + mult * Math.PI
													/ 2))
									&& width < maxDepth) {
								width++;
							}
							// or to make something spikey, try:
							// if(guide.contains(center.getX()+.1*Math.cos(angle-mult*Math.PI/2),
							// center.getY()+.1*Math.sin(angle-mult*Math.PI/2)))
							// {
							// width = random.nextFloat()*8+1;
							// }
							if (width > 1) {
								// width will be > 1 when we're on the correct
								// side AND we have
								// a fleshy material to cut into
								float crackWidth, depth;

								// now define a constant to multiply the depth
								// of the crack by
								// when width is 5, multiply by 1. when width is
								// 15, multiply by .5
								float k = -.05f * width + 1.25f;
								if (k > 1)
									k = 1; // cap at these values
								if (k < .4f)
									k = .4f;
								depth = width * k * (.5f + .5f * size)
										* (.25f + .75f * random.nextFloat());

								crackWidth = depth * depth / 150;
								if (crackWidth < 1f)
									crackWidth = 1f;
								if (crackWidth > 2)
									crackWidth = 2;

								if (distance + pendingGap - crackWidth / 2 > 0
										&& distance + pendingGap + crackWidth
												/ 2 < orig) {
									m[a].getPoint(distance + pendingGap
											+ crackWidth / 2, rightSide);
									m[a].writeShape(distance / total,
											(pendingGap - crackWidth / 2)
													/ total, writer, false);
									writer.lineTo(
											(float) (center.getX() + depth
													* Math.cos(angle + mult
															* Math.PI / 2)),
											(float) (center.getY() + depth
													* Math.sin(angle + mult
															* Math.PI / 2)));
									writer.lineTo((float) rightSide.getX(),
											(float) rightSide.getY());

									distance += pendingGap + crackWidth / 2;
									pendingGap = 0;
								}
								break;
							}
						}
					}
				}
			}
			writer.closePath();
		}
	}
}