package org.papervision3d.core.animation.curve 
{
	import org.papervision3d.core.animation.enum.InfinityType;	
	import org.papervision3d.core.animation.key.BezierCurveKey3D;	
	import org.papervision3d.core.animation.key.LinearCurveKey3D;	
	import org.papervision3d.core.animation.key.CurveKey3D;

	/**
	 * A Curve3D stores a bunch of CurveKey3D's and given a value representing an input point on a curve returns 
	 * the output of the curve for that input.
	 * 
	 * @see org.papervision3d.core.animation.key.CurveKey3D
	 * 
	 * @author Tim Knip / floorplanner.com
	 */
	public class Curve3D 
	{
		/**
		 * The behavior of the curve after the last key. 
		 * 
		 * @see org.papervision3d.core.animation.enum.InfinityType
		 */
		public var postInfinity : uint;
		
		/**
		 * The behavior of the curve before the first key. 
		 * 
		 * @see org.papervision3d.core.animation.enum.InfinityType
		 */
		public var preInfinity : uint;
		
		/**
		 * 
		 */
		protected var _keys : Array;
		
		/** */
		private var _current : int = 0;

		/**
		 * Constructor.
		 * 
		 * @param preInfinity	The behavior of the curve before the first key. Defaults to InfinityType.CONSTANT
		 * @param postInfinity	The behavior of the curve after the last key. Defaults to InfinityType.CONSTANT
		 * 
		 * @see org.papervision3d.core.animation.enum.InfinityType
		 */
		public function Curve3D(preInfinity : int = -1, postInfinity : int = -1)
		{
			this.postInfinity = postInfinity < 0 ? InfinityType.CONSTANT : postInfinity; 
			this.preInfinity = preInfinity < 0 ? InfinityType.CONSTANT : preInfinity;
			
			_keys = new Array();
			_current = 0;
		}
		
		/**
		 * Adds a key.
		 * 
		 * @param key
		 * 
		 * @return The added key or null on failure
		 */
		public function addKey(key : CurveKey3D) : CurveKey3D
		{
			if(_keys.indexOf(key) == -1)
			{
				_keys.push(key);	
				return key;
			}
			return null;
		}

		/**
		 * Removes a key.
		 * 
		 * @param key
		 * 
		 * @return The removed key or null on failure
		 */
		public function removeKey(key : CurveKey3D) : CurveKey3D
		{
			var pos : int = _keys.indexOf(key);
			if(pos >= 0)
			{
				_keys.splice(pos, 1);
				return key;	
			}
			return null;	
		}
		
		/**
		 * Main workhorse of the animation system.
		 * 
		 * @param input	Time in seconds.
		 * 
		 * @return	The current value of this curve.
		 */
		public function evaluate(input : Number) : Number
		{
			var kfs : Array = _keys;
			var numKeys : int = kfs.length;
			
			// Check for empty curves and poses (curves with 1 key).
			if( numKeys == 0 ) return 0.0;
			if( numKeys == 1 ) return kfs[0].output;
			
			var index:int;
			var startKey : CurveKey3D = kfs[0];
			var endKey : CurveKey3D = kfs[numKeys-1];
			var outputStart:Number = startKey.output;
			var outputEnd:Number = endKey.output;
			var inputStart:Number = startKey.input;
			var inputEnd:Number = endKey.input;
			var inputSpan:Number = inputEnd - inputStart;
			var outputSpan:Number = outputEnd - outputStart;
			var cycleCount:Number;

			// Account for pre-infinity mode
			var outputOffset:Number = 0.0;
			
			if(input < inputStart)
			{
				switch(preInfinity)
				{
					case InfinityType.CONSTANT: 
						return outputStart;
					case InfinityType.LINEAR: 
						return outputStart + (input - inputStart) * (kfs[1].output - outputStart) / (kfs[1].input - inputStart);
					case InfinityType.CYCLE:
						cycleCount = Math.ceil((inputStart - input) / inputSpan); 
						input += cycleCount * inputSpan; 
						break;
					case InfinityType.CYCLE_RELATIVE:
						cycleCount = Math.ceil((inputStart - input) / inputSpan); 
						input += cycleCount * inputSpan; 
						outputOffset -= cycleCount * outputSpan;
						break;
					case InfinityType.OSCILLATE:
						cycleCount = Math.ceil((inputStart - input) / (2.0 * inputSpan)); 
						input += cycleCount * 2.0 * inputSpan; 
						input = inputEnd - Math.abs(input - inputEnd); 
						break;
					default: 
						return outputStart;
				}
			}
			else if(input >= inputEnd)
			{
				// Account for post-infinity mode
				switch (postInfinity)
				{
					case InfinityType.CONSTANT: 
						return outputEnd;
					case InfinityType.LINEAR: 
						return outputEnd + (input - inputEnd) * (kfs[numKeys-2].output - outputEnd) / (kfs[numKeys-2].input - inputEnd);
					case InfinityType.CYCLE:
						cycleCount = Math.ceil((input - inputEnd) / inputSpan);
						input -= cycleCount * inputSpan; 
						break;
					case InfinityType.CYCLE_RELATIVE:
						cycleCount = Math.ceil((input - inputEnd) / inputSpan); 
						input -= cycleCount * inputSpan; 
						outputOffset += cycleCount * outputSpan; 
						break;
					case InfinityType.OSCILLATE:
						cycleCount = Math.ceil((input - inputEnd) / (2.0 * inputSpan)); 
						input -= cycleCount * 2.0 * inputSpan; 
						input = inputStart + Math.abs(input - inputStart); 
						break;
					default: 
						return outputEnd;
				}
			}
			
			
			var kf : CurveKey3D = _current < numKeys ? kfs[_current] : null;
			var cf : CurveKey3D = _current - 1 >= 0 ? kfs[_current-1] : null;
			
			// might speed up interval search a bit
			_current = (cf && kf && cf.input < input) ? _current : 0;
			
			// Find the current interval
			// TODO: speed up interval search
			for( index = _current; index < numKeys; index++ )
			{
				kf = kfs[index];
				if(kf.input > input)
				{
					break;	
				}
			}
			
			_current = index;
			
			if(index < 1)
			{
				return outputOffset + outputStart;
			}
			
			// Get the keys and values for this interval
			endKey = kfs[index];
			startKey = kfs[index - 1];
			
			var endValue:Number = endKey.output;
			var startValue:Number = startKey.output;
			var inputInterval : Number = endKey.input - startKey.input;
			var outputInterval : Number = endValue - startValue;
			var output:Number;
			
			// TODO: handle Bezier curves.
			if(startKey is LinearCurveKey3D || startKey is BezierCurveKey3D)
			{
				output = startValue + ((input - startKey.input) / inputInterval) * outputInterval;
			}
			else
			{
				output = startValue;
			}

			return outputOffset + output;	
		}
		
		/**
		 * Clone.
		 * 
		 * @return	The cloned curve.
		 */
		public function clone() : Curve3D
		{
			var curve : Curve3D = new Curve3D(this.preInfinity, this.postInfinity);
			var key : CurveKey3D;
			var i : int;
			
			for(i = 0; i < _keys.length; i++)
			{
				key = _keys[i];
				curve.addKey(key.clone());
			}
			
			return curve;	
		}
		
		/**
		 * 
		 */
		public function get keys() : Array
		{
			return _keys;
		}
	}
}
