/*
 * Copyright (c) 2005-2010 Trident Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of Trident Kirill Grouchnikov nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.pushingpixels.trident.ease;

import java.util.ArrayList;

/**
 * Spline easer. Is based on the code from <a
 * href="https://timingframework.dev.java.net">TimingFramework</a> by Chet Haase
 * and Romain Guy.
 * 
 * @author Kirill Grouchnikov
 */
public class Spline implements TimelineEase {
	// private float easeAmount;

	public Spline(float easeAmount) {
		this(easeAmount, 0, 1 - easeAmount, 1);
		// this.easeAmount = easeAmount;
	}

	private static class FloatPoint {
		public float x;
		public float y;

		public FloatPoint(float x, float y) {
			this.x = x;
			this.y = y;
		}
	}

	// Note: (x0,y0) and (x1,y1) are implicitly (0, 0) and (1,1) respectively
	private float x1, y1, x2, y2;
	private ArrayList lengths = new ArrayList();

	/**
	 * Creates a new instance of SplineInterpolator with the control points
	 * defined by (x1, y1) and (x2, y2). The anchor points are implicitly
	 * defined as (0, 0) and (1, 1).
	 * 
	 * @throws IllegalArgumentException
	 *             This exception is thrown when values beyond the allowed [0,1]
	 *             range are passed in
	 */
	public Spline(float x1, float y1, float x2, float y2) {
		if (x1 < 0 || x1 > 1.0f || y1 < 0 || y1 > 1.0f || x2 < 0 || x2 > 1.0f
				|| y2 < 0 || y2 > 1.0f) {
			throw new IllegalArgumentException("Control points must be in "
					+ "the range [0, 1]:");
		}

		this.x1 = x1;
		this.y1 = y1;
		this.x2 = x2;
		this.y2 = y2;

		// Now contruct the array of all lengths to t in [0, 1.0]
		float prevX = 0.0f;
		float prevY = 0.0f;
		float prevLength = 0.0f; // cumulative length
		for (float t = 0.01f; t <= 1.0f; t += .01f) {
			FloatPoint xy = getXY(t);
			float length = prevLength
					+ (float) Math.sqrt((xy.x - prevX) * (xy.x - prevX)
							+ (xy.y - prevY) * (xy.y - prevY));
			LengthItem lengthItem = new LengthItem(length, t);
			lengths.add(lengthItem);
			prevLength = length;
			prevX = xy.x;
			prevY = xy.y;
		}
		// Now calculate the fractions so that we can access the lengths
		// array with values in [0,1]. prevLength now holds the total
		// length of the spline.
		for (int i = 0; i < lengths.size(); ++i) {
			LengthItem lengthItem = (LengthItem) lengths.get(i);
			lengthItem.setFraction(prevLength);
		}
	}

	/**
	 * Calculates the XY point for a given t value.
	 * 
	 * The general spline equation is: x = b0*x0 + b1*x1 + b2*x2 + b3*x3 y =
	 * b0*y0 + b1*y1 + b2*y2 + b3*y3 where: b0 = (1-t)^3 b1 = 3 * t * (1-t)^2 b2
	 * = 3 * t^2 * (1-t) b3 = t^3 We know that (x0,y0) == (0,0) and (x1,y1) ==
	 * (1,1) for our splines, so this simplifies to: x = b1*x1 + b2*x2 + b3 y =
	 * b1*x1 + b2*x2 + b3
	 * 
	 * @param t
	 *            parametric value for spline calculation
	 */
	private FloatPoint getXY(float t) {
		FloatPoint xy;
		float invT = (1 - t);
		float b1 = 3 * t * (invT * invT);
		float b2 = 3 * (t * t) * invT;
		float b3 = t * t * t;
		xy = new FloatPoint((b1 * x1) + (b2 * x2) + b3, (b1 * y1)
				+ (b2 * y2) + b3);
		return xy;
	}

	/**
	 * Utility function: When we are evaluating the spline, we only care about
	 * the Y values. See {@link #getXY(float)} for the details.
	 */
	private float getY(float t) {
		FloatPoint xy;
		float invT = (1 - t);
		float b1 = 3 * t * (invT * invT);
		float b2 = 3 * (t * t) * invT;
		float b3 = t * t * t;
		return (b1 * y1) + (b2 * y2) + b3;
	}

	/**
	 * Given a fraction of time along the spline (which we can interpret as the
	 * length along a spline), return the interpolated value of the spline. We
	 * first calculate the t value for the length (by doing a lookup in our
	 * array of previousloy calculated values and then linearly interpolating
	 * between the nearest values) and then calculate the Y value for this t.
	 * 
	 * @param lengthFraction
	 *            Fraction of time in a given time interval.
	 * @return interpolated fraction between 0 and 1
	 */
	@Override
    public float map(float lengthFraction) {
		// REMIND: speed this up with binary search
		float interpolatedT = 1.0f;
		float prevT = 0.0f;
		float prevLength = 0.0f;
		for (int i = 0; i < lengths.size(); ++i) {
			LengthItem lengthItem = (LengthItem) lengths.get(i);
			float fraction = lengthItem.getFraction();
			float t = lengthItem.getT();
			if (lengthFraction <= fraction) {
				// answer lies between last item and this one
				float proportion = (lengthFraction - prevLength)
						/ (fraction - prevLength);
				interpolatedT = prevT + proportion * (t - prevT);
				return getY(interpolatedT);
			}
			prevLength = fraction;
			prevT = t;
		}
		return getY(interpolatedT);
	}
}

/**
 * Struct used to store information about length values. Specifically, each item
 * stores the "length" (which can be thought of as the time elapsed along the
 * spline path), the "t" value at this length (used to calculate the (x,y) point
 * along the spline), and the "fraction" which is equal to the length divided by
 * the total absolute length of the spline. After we calculate all LengthItems
 * for a give spline, we have a list of entries which can return the t values
 * for fractional lengths from 0 to 1.
 */
class LengthItem {
	float length;
	float t;
	float fraction;

	LengthItem(float length, float t, float fraction) {
		this.length = length;
		this.t = t;
		this.fraction = fraction;
	}

	LengthItem(float length, float t) {
		this.length = length;
		this.t = t;
	}

	public float getLength() {
		return length;
	}

	public float getT() {
		return t;
	}

	public float getFraction() {
		return fraction;
	}

	void setFraction(float totalLength) {
		fraction = length / totalLength;
	}
}
