/*
Plugin Name: amCharts Animate
Description: Smoothly animates the `dataProvider`
Author: Paul Chapman, amCharts
Version: 1.1.1
Author URI: http://www.amcharts.com/

Copyright 2015 amCharts

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Please note that the above license covers only this plugin. It by all means does
not apply to any other amCharts products that are covered by different licenses.
*/

/* globals AmCharts */
/* jshint -W061 */

( function() {
	"use strict";


	// For older browsers, e.g. IE9 and lower
	if ( typeof requestAnimationFrame === "undefined" ) {
		var fps = 1000 / 60;

		var raf = function( f ) {
			setTimeout( function() {
				f( new Date().getTime() );
			}, fps );
		};

	} else {
		var raf = requestAnimationFrame;
	}


	function tween( time, from, to ) {
		return ( time * ( to - from ) ) + from;
	}


	function easeInOut3( t ) {
		var r = ( t < 0.5 ? t * 2 : ( 1 - t ) * 2 );
		r *= r * r * r;
		return ( t < 0.5 ? r / 2 : 1 - ( r / 2 ) );
	}

	function easeIn3( t ) {
		t *= t * t * t;
		return t;
	}

	function easeOut3( t ) {
		var r = ( 1 - t );
		r *= r * r * r;
		return ( 1 - r );
	}


	function Tween( object, key, from, to ) {
		this._object = object;
		this._key = key;
		this._from = from;
		this._to = to;
	}

	Tween.prototype.interpolate = function( time ) {
		this._object[ this._key ] = tween( time, this._from, this._to );
	};


	function Animation( duration, easing, onComplete, tweens, chart ) {
		this._finished = false;
		this._startTime = null;

		this._duration = duration;
		this._easing = ( easing == null ? easeOut3 : easing );
		this._onComplete = onComplete;
		this._tweens = tweens;

		this._chart = chart;
	}

	Animation.prototype.cancel = function() {
		this._finished = true;
		this._startTime = null;

		this._duration = null;
		this._easing = null;
		this._onComplete = null;
		this._tweens = null;

		this._chart = null;
	};

	Animation.prototype._onFrame = function( now ) {
		// This will only happen when the animation was cancelled
		if ( this._finished ) {
			return true;

		} else if ( this._startTime === null ) {
			this._startTime = now;
			return false;

		} else {
			var diff = now - this._startTime;

			if ( diff < this._duration ) {
				this._tick( diff / this._duration );
				return false;

			} else {
				this._end( 1 );
				// Cleanup all the properties
				this.cancel();
				return true;
			}
		}
	};

	Animation.prototype._tick = function( time ) {
		// Apply the easing to the time ratio
		time = this._easing( time );

		var tweens = this._tweens;

		for ( var i = 0; i < tweens.length; ++i ) {
			tweens[ i ].interpolate( time );
		}

		// TODO check the performance of this
		pushNew( needsValidation, this._chart );
	};

	Animation.prototype._end = function( time ) {
		this._tick( time );

		this._onComplete();
	};


	function Animator() {
		this._animating = false;
		this._animations = [];
		this._onBeforeFrames = [];
		this._onAfterFrames = [];

		var self = this;

		this._raf = function( now ) {
			self._onFrame( now );
		};
	}

	Animator.prototype.animate = function( animation ) {
		this._animations.push( animation );

		if ( !this._animating ) {
			this._animating = true;

			raf( this._raf );
		}
	};


	Animator.prototype.onBeforeFrame = function( f ) {
		this._onBeforeFrames.push( f );
	};

	Animator.prototype.onAfterFrame = function( f ) {
		this._onAfterFrames.push( f );
	};


	Animator.prototype._onFrame = function( now ) {
		var onBeforeFrames = this._onBeforeFrames;

		for ( var i = 0; i < onBeforeFrames.length; ++i ) {
			onBeforeFrames[ i ]( now );
		}


		var animations = this._animations;

		for ( var i = 0; i < animations.length; ++i ) {
			var animation = animations[ i ];

			// If the animation is finished...
			if ( animation._onFrame( now ) ) {
				// TODO this is a bit slow, but I don't know of a faster alternative
				animations.splice( i, 1 );
				--i;
			}
		}


		var onAfterFrames = this._onAfterFrames;

		for ( var i = 0; i < onAfterFrames.length; ++i ) {
			onAfterFrames[ i ]( now );
		}


		// All animations are finished
		if ( animations.length === 0 ) {
			this._animating = false;

		} else {
			raf( this._raf );
		}
	};


	var _animator = new Animator();


	var needsValidation = [];

	// This is more robust than the built-in `isNaN` function
	function isNaN( x ) {
		return x !== x;
	}

	function each( array, fn ) {
		for ( var i = 0; i < array.length; ++i ) {
			fn( array[ i ] );
		}
	}

	function pushNew( array, x ) {
		for ( var i = 0; i < array.length; ++i ) {
			if ( array[ i ] === x ) {
				return;
			}
		}

		array.push( x );
	}

	// TODO check the performance of this
	_animator.onAfterFrame( function() {
		for ( var i = 0; i < needsValidation.length; ++i ) {
			needsValidation[ i ].validateData();
		}

		needsValidation.length = 0;
	} );


	// This ensures that a key is only added once
	function addKey( keys, seen, key ) {
		if ( !seen[ key ] ) {
			seen[ key ] = true;
			keys.push( key );
		}
	}

	function addKeys( keys, seen, object, a ) {
		each( a, function( key ) {
			var value = object[ key ];

			if ( value != null ) {
				addKey( keys, seen, value );
			}
		} );
	}


	function getKeysSliced( chart, keys, seen ) {
		addKeys( keys, seen, chart, [
			"alphaField",
			"valueField"
		] );
	}

	function getKeysFunnel( chart, keys, seen ) {
		getKeysSliced( chart, keys, seen );
	}

	function getKeysPie( chart, keys, seen ) {
		getKeysSliced( chart, keys, seen );

		addKeys( keys, seen, chart, [
			"labelRadiusField"
		] );
	}

	function getKeysGraph( graph, keys, seen ) {
		addKeys( keys, seen, graph, [
			"alphaField",
			"bulletSizeField",
			"closeField",
			"dashLengthField",
			"errorField",
			"highField",
			"lowField",
			"openField",
			"valueField"
		] );
	}

	function getKeysXY( graph, keys, seen ) {
		getKeysGraph( graph, keys, seen );

		addKeys( keys, seen, graph, [
			"xField",
			"yField"
		] );
	}

	function getKeysGraphs( graphs, keys, seen, f ) {
		each( graphs, function( graph ) {
			f( graph, keys, seen );
		} );
	}

	function getKeysCategoryAxis( categoryAxis, keys, seen ) {
		addKeys( keys, seen, categoryAxis, [
			"widthField"
		] );
	}


	// Returns an array of all of the animatable keys
	function getKeys( chart ) {
		var keys = [];

		var seen = {};

		if ( chart.type === "funnel" ) {
			getKeysFunnel( chart, keys, seen );

		} else if ( chart.type === "pie" ) {
			getKeysPie( chart, keys, seen );

		} else if ( chart.type === "serial" ) {
			getKeysCategoryAxis( chart.categoryAxis, keys, seen );
			getKeysGraphs( chart.graphs, keys, seen, getKeysGraph );

		} else if ( chart.type === "radar" ) {
			getKeysGraphs( chart.graphs, keys, seen, getKeysGraph );

		} else if ( chart.type === "xy" ) {
			getKeysGraphs( chart.graphs, keys, seen, getKeysXY );
		}

		return keys;
	}


	// Sets the minimum/maximum of the value axes while the animation is playing
	function setAxesMinMax( chart ) {
		var axes = {};

		if ( chart.type === "serial" || chart.type === "radar" || chart.type === "xy" ) {
			each( chart.valueAxes, function( axis ) {
				// TODO is it guaranteed that every value axis has an id ?
				if ( axes[ axis.id ] == null ) {
					axes[ axis.id ] = {
						minimum: axis.minimum,
						maximum: axis.maximum
					};

					// TODO is this correct ?
					if ( axis.minimum == null ) {
						axis.minimum = axis.min;
					}

					if ( axis.maximum == null ) {
						axis.maximum = axis.max;
					}
				}
			} );
		}

		return axes;
	}

	// Resets the minimum/maximum of the value axes after the animation is finished
	function resetAxesMinMax( chart, axes ) {
		if ( chart.type === "serial" || chart.type === "radar" || chart.type === "xy" ) {
			each( chart.valueAxes, function( axis ) {
				var info = axes[ axis.id ];

				if ( info != null ) {
					if ( info.minimum == null ) {
						delete axis.minimum;
					}

					if ( info.maximum == null ) {
						delete axis.maximum;
					}
				}
			} );
		}
	}


	function getCategoryField( chart ) {
		if ( chart.type === "funnel" || chart.type === "pie" ) {
			return chart.titleField;

		} else if ( chart.type === "serial" || chart.type === "radar" ) {
			return chart.categoryField;
		}
	}


	function getValue( object, key ) {
		var value = object[ key ];

		if ( value == null ) {
			return null;

		} else {
			value = +value;

			// TODO test this
			// TODO what about Infinity, etc. ?
			if ( isNaN( value ) ) {
				return null;

			} else {
				return value;
			}
		}
	}

	function getCategory( object, key ) {
		var value = object[ key ];

		if ( value == null ) {
			return null;

		} else {
			// TODO better algorithm for this ?
			return "" + value;
		}
	}


	function getCategories( dataProvider, categoryField ) {
		var categories = {};

		each( dataProvider, function( data ) {
			var category = getCategory( data, categoryField );

			if ( category != null ) {
				categories[ category ] = data;
			}
		} );

		return categories;
	}


	function getNormalTweens( dataProvider, categoryField, categories, keys ) {
		var tweens = [];

		each( dataProvider, function( newData ) {
			var category = getCategory( newData, categoryField );

			// If the new data has the same category as the old data...
			if ( category != null && category in categories ) {
				var oldData = categories[ category ];

				each( keys, function( key ) {
					var oldValue = getValue( oldData, key );
					var newValue = getValue( newData, key );

					// If the old field and new field both exist...
					if ( oldValue != null && newValue != null ) {
						tweens.push( new Tween( newData, key, oldValue, newValue ) );
					}
				} );
			}
		} );

		return tweens;
	}


	function getXYTweens( oldDataProvider, newDataProvider, keys ) {
		var tweens = [];

		var length = Math.min( oldDataProvider.length, newDataProvider.length );

		for ( var i = 0; i < length; ++i ) {
			var oldData = oldDataProvider[ i ];
			var newData = newDataProvider[ i ];

			each( keys, function( key ) {
				var oldValue = getValue( oldData, key );
				var newValue = getValue( newData, key );

				// If the old field and new field both exist...
				if ( oldValue != null && newValue != null ) {
					tweens.push( new Tween( newData, key, oldValue, newValue ) );
				}
			} );
		}

		return tweens;
	}


	function getTweens( chart, dataProvider ) {
		if ( chart.type === "xy" ) {
			var keys = getKeys( chart );

			return getXYTweens( chart.dataProvider, dataProvider, keys );

		} else {
			var categoryField = getCategoryField( chart );
			var keys = getKeys( chart );

			var categories = getCategories( chart.dataProvider, categoryField );

			return getNormalTweens( dataProvider, categoryField, categories, keys );
		}
	}


	function animateData( dataProvider, options ) {
		var chart = this;

		var tweens = getTweens( chart, dataProvider );

		var axes = setAxesMinMax( chart );

		chart.dataProvider = dataProvider;

		function onComplete() {
			resetAxesMinMax( chart, axes );

			if ( options.complete != null ) {
				options.complete();
			}
		}

		var animation = new Animation(
			options.duration,
			options.easing,
			onComplete,
			tweens,
			chart
		);

		_animator.animate( animation );

		return animation;
	}


	AmCharts.addInitHandler( function( chart ) {
		chart.animateData = animateData;
	}, [ "funnel", "pie", "serial", "radar", "xy" ] );

} )();
