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

import java.awt.Dimension;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Rectangle2D;
import java.util.HashMap;
import java.util.Map;

import com.pump.geom.ShapeBounds;

/**
 * This takes any abstract silhouette and will zoom it in/out to reveal the new
 * frame.
 * 
 * <P>
 * This class is actually a little bit clever, because it will study exactly to
 * what proportion it has to zoom the silhouette to completely cover the frame
 * size.
 * 
 */
public abstract class AbstractShapeTransition2D extends Transition2D {

	int type;

	/**
	 * Creates a new AbstractShapeTransition2D that zooms out
	 * 
	 */
	public AbstractShapeTransition2D() {
		this(OUT);
	}

	/**
	 * Creates a new AbstractShapeTransition2D
	 * 
	 * @param type
	 *            must be IN or OUT.
	 *            <P>
	 *            This indicates whether the shape grows or shrinks as this
	 *            transition progresses.
	 */
	public AbstractShapeTransition2D(int type) {
		if (!(type == IN || type == OUT))
			throw new IllegalArgumentException("Type must be IN or OUT.");
		this.type = type;
	}

	public abstract Shape getShape();

	Map<Dimension, Number> multipliers = new HashMap<Dimension, Number>();

	/**
	 * Calculating the scaling ratio for the shape to fit the dimensions
	 * provided.
	 */
	protected float calculateMultiplier(Dimension size) {
		Shape shape = getShape();
		Area base = new Area(shape);
		AffineTransform transform = new AffineTransform();
		Rectangle2D r = ShapeBounds.getBounds(base);
		transform.translate(size.width / 2f - r.getCenterX(), size.height / 2f
				- r.getCenterY());
		base.transform(transform);
		r = ShapeBounds.getBounds(base, r);
		float min = 0;
		float max = 1;
		Rectangle2D boundsRect = new Rectangle2D.Float(0, 0, size.width,
				size.height);
		while (isOK(base, r, boundsRect, max) == false) {
			min = max;
			max *= 1.2;
		}
		float f = calculateMultiplier(base, r, boundsRect, min, max);
		isOK(base, r, boundsRect, f);
		return f;
	}

	/** Perform a binary search for the best-fitting multiplier to use */
	private float calculateMultiplier(Area shape, Rectangle2D shapeBounds,
			Rectangle2D bounds, float min, float max) {
		if (max - min < .5)
			return max;

		float middle = (min + max) / 2f;
		if (isOK(shape, shapeBounds, bounds, middle)) {
			return calculateMultiplier(shape, shapeBounds, bounds, min, middle);
		} else {
			return calculateMultiplier(shape, shapeBounds, bounds, middle, max);
		}
	}

	/** Determine if a particular scaling ratio works */
	private boolean isOK(Area shape, Rectangle2D shapeBounds,
			Rectangle2D bounds, float ratio) {
		Area area = new Area(shape);
		area.transform(AffineTransform.getScaleInstance(ratio, ratio));
		Rectangle2D r = ShapeBounds.getBounds(area);
		area.transform(AffineTransform.getTranslateInstance(-r.getCenterX()
				+ bounds.getCenterX(), -r.getCenterY() + bounds.getCenterY()));

		Area boundsArea = new Area(bounds);
		boundsArea.subtract(area);
		return boundsArea.isEmpty();
	}

	@Override
	public Transition2DInstruction[] getInstructions(float progress,
			Dimension size) {
		Number multiplier = multipliers.get(size);
		if (multiplier == null) {
			multiplier = new Float(calculateMultiplier(size));
			multipliers.put(size, multiplier);
		}

		if (type == IN) {
			progress = 1 - progress;
		}

		Shape clipping = getShape();
		Rectangle2D r = ShapeBounds.getBounds(clipping);

		AffineTransform transform = new AffineTransform();

		transform.setToIdentity();

		transform.translate(size.width / 2, size.height / 2);
		transform.scale(progress * multiplier.floatValue(), progress
				* multiplier.floatValue());
		transform.translate(-size.width / 2, -size.height / 2);

		transform.translate(-r.getCenterX() + size.width / 2f, -r.getCenterY()
				+ size.height / 2f);

		clipping = transform.createTransformedShape(clipping);

		return new Transition2DInstruction[] {
				new ImageInstruction(type == OUT),
				new ImageInstruction(type != OUT, null, clipping) };
	}

	public abstract String getShapeName();

	@Override
	public String toString() {
		if (type == IN) {
			return getShapeName() + " In";
		}
		return getShapeName() + " Out";
	}
}