sense.ui.effect.Tween = Class.create(sense.ui.effect.Base, {

	// Methods
	initialize: function($super, element, options) {
		$super(element, options);

		this.queue = [];
		this.queueIndex = 0;
		this.queueIsLooped = false;

		this.isAnimating;

		if (options) this.add(options);
	},

	add: function(options) {
		// Add a set of tween options to the queue
		this.queue.push(options);
		// If there isn't a tween already running, run this one
		if (!this.isAnimating) this.playNext();
		// Return the current instance so method calls can be chained
		return this;
	},

	pause: function(s) {
		this.add({ seconds: (s || 1) });
		return this;
	},

	loop: function() {
		this.queueIsLooped = true;
		return this;
	},

	playNext: function() {
		// If there are tweens in the queue, remove the 1st one and run it
		if (this.queueIndex < this.queue.length) {
			this.play(this.queue[this.queueIndex]);
			this.queueIndex++;
		} else if (this.queueIsLooped) {
			this.queueIndex = 0;
			this.playNext();
		}
	},

	play: function(options) {
		this.options = Object.extend({
			styleFrom: {},
			styleTo: {},
			transition: 'quadInOut',
			seconds: 1,
			fps: 24,
			onStart: null,
			onUpdate: null,
			onEnd: null,
			resetAfter: false
		}, options);

		this.step = 0;
		this.steps = this.options.seconds * this.options.fps;
		this.delay = 1000 / this.options.fps;

		// Allow transitions to be specified as strings
		if (typeof this.options.transition == 'string') {
			this.options.transition = this.parseTransitionString(this.options.transition);
		}

		// Allow a shorthand for simple style definitions
		if (this.options.style) this.options.styleTo = this.options.style;

		// Get a master list of all styles which will be affected by this tween
		this.styleProperties = {};
		for (var key in this.options.styleFrom) this.styleProperties[key] = true;
		for (var key in this.options.styleTo) 	this.styleProperties[key] = true;

		// Reset in case we're already in the middle of a tween
		this.reset();

		// Styles should be in "from"/"to" pairs. If either is missing for a given
		// property, fill it in with the element's current style as a default.
		for (var key in this.styleProperties) {
			var val = (key == 'backgroundColor' || key == 'background') ? this.getBackgroundColor() : this.element.getStyle(key) || 0;
			if (typeof this.options.styleFrom[key] == 'undefined') this.options.styleFrom[key] = val;
			if (typeof this.options.styleTo[key] == 'undefined') this.options.styleTo[key] = val;
		}
		
		// console.log(this.options.styleFrom);
		// console.log(this.options.styleTo);

		this.prepare();
		this.animate();
	},

	reset: function() {
		// Can be called either just before, or just after a tween.
		// Clear any existing animation to prevent a juddering overlap.
		if (this.element.TweenTimeoutID) {
			clearTimeout(this.element.TweenTimeoutID);
			this.element.TweenTimeoutID = null;
		}
		if (this.options.resetAfter) {
			if (this.element.originalStyles) {
				this.element.setStyle(this.element.originalStyles);
			} else {
				this.element.originalStyles = {};
				for (var key in this.styleProperties) {
					// For most properties, don't use getStyle() because we want to take the values 
					// literally, and not introduce any interpretation, so we can set them back 
					// exactly as they were. In the case of opacity though, this interpretation is to
					// our benefit, since it smooths over cross browser inconsistencies.
					this.element.originalStyles[key] = (key == 'opacity') ? this.element.getStyle(key) : this.element.style[key];
				}
			}
		}
		this.isAnimating = false;
	},

	prepare: function() {
		if (typeof this.options.onStart == 'function') {
			this.options.onStart.call(this);
		}
		this.isAnimating = true;
	},

	animate: function() {
		if (this.step <= this.steps) {

			// If we're not done yet, style the element with its transitioned 
			// properties, set a timeout to call another iteration.

			this.element.setStyle(this.interpolateStyles());
			this.element.TweenTimeoutID = setTimeout(this.animate.bind(this), this.delay);

			if (typeof this.options.onUpdate == 'function') {
				this.options.onUpdate.call(this);
			}

		} else {

			// If we're finished animating, do some upkeep and then 
			// move onto the next tween in the queue if there is one.
			this.reset();

			if (typeof this.options.onEnd == 'function') {
				this.options.onEnd.call(this);
			}

			this.playNext();

		}
	},

	parseTransitionString: function(str){
		// For instance: 
		// 'linear' 	-> sense.ui.effect.easing.linear
		// 'cubicInOut' -> sense.ui.effect.easing.cubic.easeInOut
		var matches = str.match(/^([a-z]+)(In|Out|InOut)$/);
		if (matches && matches.length == 3) {
			var set = matches[1];
			var dir = matches[2];
			return sense.ui.effect.easing[set]['ease' + dir];
		} else {
			return sense.ui.effect.easing[str];
		}
	},
	
	interpolateStyles: function() {
		// Interpolate each of the style pairs in turn, using
		// a pre-computed scale ratio for the current step.
		var styles = {};
		var ratio = this.options.transition(this.step++ / this.steps);
		var a,b;

		for (var key in this.options.styleFrom) {
			a = this.options.styleFrom[key];
			b = this.options.styleTo[key];
			styles[key] = this.interpolateStyle(a,b, ratio);
		}

		return styles;
	},

	interpolateStyle: function(a,b, ratio) {
		// Just a couple of rough regexs to see how we should
		// treat the values for this style attribute. Don't have
		// to match perfectly, just have to guess the style type.

		var colorRegex = /^(rgba?\([0-9, ]*\)|#[0-9a-f]+)$/i;
		var unitsRegex = /^-?[0-9.]+(px|em|%)$/i;

		if (colorRegex.test(a) || colorRegex.test(b)) {
			return this.interpolateColor(a,b, ratio);
		}
		if (unitsRegex.test(a) || unitsRegex.test(b)) {
			return this.interpolateUnits(a,b, ratio);
		}

		return this.interpolate(a,b, ratio);
	},

	interpolateColor: function(a,b, ratio) {
		// Need to parse the color definition into an RGB triplet,
		// and then interpolate each triplet-part independently.

		var rgbA = this.parseColor(a);
		var rgbB = this.parseColor(b);

		var r = Math.round(this.interpolate(rgbA[0],rgbB[0], ratio));
		var g = Math.round(this.interpolate(rgbA[1],rgbB[1], ratio));
		var b = Math.round(this.interpolate(rgbA[2],rgbB[2], ratio));

		return 'rgb('+ r +','+ g +','+ b +')';
	},

	interpolateUnits: function(a,b, ratio) {
		// Find out which sort of length unit we're dealing with,
		// presume they're both of the same type, since we can't
		// easily transition between them otherwise.

		var units = '';

		if (/-?[0-9.]+px/.test(b)) units = 'px';
		if (/-?[0-9.]+em/.test(b)) units = 'em';
		if (/-?[0-9.]+%/.test(b))  units = '%';

		a = parseInt(a,10);
		b = parseInt(b,10);

		return this.interpolate(a,b, ratio) + units;
	},

	interpolate: function(a,b, ratio) {
		// Start point, plus delta to end point, scaled by a ratio
		return a + ((b-a) * ratio);
	},

	isTransparentColor: function(clr) {
		// Usually just a string, but WebKit returns a fully-transparent RGBa black
		return (clr == 'transparent' || clr == 'rgba(0, 0, 0, 0)');
	},

	getBackgroundColor: function() {
		var clr = this.element.getStyle('backgroundColor');

		// If this element's background is transparent, look 
		// for a color one on each of its ancestors instead.
		if (this.isTransparentColor(clr)) {
			var ancestors = this.element.ancestors();
			for (var i=0; i<ancestors.length; i++) {
				clr = ancestors[i].getStyle('backgroundColor');
				if (!this.isTransparentColor(clr)) break;
			}
		}

		// If there isn't even a solid bg-color on the <body> tag, just default to white
		if (this.isTransparentColor(clr)) {
			clr = '#fff';
		}

		return clr;
	},

	parseColor: function(color) {
		// Convert a string something like '#FFFFFF' or 'rgb(255,255,255)' to [255,255,255]
		// From: http://www.meyerweb.com/eric/tools/color-blend/

		var matches = color.match(/#([0-9a-f]+)/i);
		if (matches) var hex = matches[1];
		var matches = color.match(/rgba?\(([0-9]+, *[0-9]+, *[0-9]+)(, *[0-9]+)?\)/i);
		if (matches) var rgb = matches[1];

		if (rgb) {
			var num = rgb.split(',');
			var base = 10;
		} 
		if (hex) {
			if (hex.length == 3) {
				var r = hex.substr(0,1);
				var g = hex.substr(1,1);
				var b = hex.substr(2,1);
				var hex = r+r + g+g + b+b;
			}
			var num = [hex.substr(0,2), hex.substr(2,2), hex.substr(4,2)];
			var base = 16;
		}

		return [ parseInt(num[0],base), parseInt(num[1],base), parseInt(num[2],base) ];
	},

	//==============================================================================
	// Some simple built-in effects with sensible defaults
	//==============================================================================

	highlight: function(options){
		var defaults = {
			styleFrom: { backgroundColor:'#ff9' },
			transition: 'linear',
			resetAfter: true
		};

		return this.add(this.extendDefaults(defaults, options));
	},

	fadeIn: function(options){
		var defaults = {
			styleFrom: { opacity:0 },
			resetAfter: true,
			seconds: 0.5,
			onStart: function(proceed){
				// this.element.setStyle({ visibility:'visible' });
				this.element.show();
				if (proceed) proceed();
			}
		};

		return this.add(this.extendDefaults(defaults, options));
	},

	fadeOut: function(options){
		var defaults = {
			styleTo:   { opacity:0 },
			resetAfter: true,
			seconds: 0.5,
			onEnd: function(proceed){
				// this.element.setStyle({ visibility:'hidden' });
				this.element.hide();
				if (proceed) proceed();
			}
		};

		return this.add(this.extendDefaults(defaults, options));
	},

	blindDown: function(options){
		var defaults = {
			styleFrom: { height:'0px' },
			styleTo: { height: this.getHeightWithMargins(this.element) + 'px' },
			resetAfter: true,
			seconds: 0.5,
			onStart: this.blindWrap,
			onEnd: this.blindUnwrap
		};

		return this.add(this.extendDefaults(defaults, options));
	},

	blindUp: function(options){
		var defaults = {
			styleFrom: { height: this.element.getHeight() + 'px' },
			styleTo: { height:'0px' },
			seconds: 0.5,
			onStart: this.blindWrap,
			onEnd: this.blindUnwrap
		};

		return this.add(this.extendDefaults(defaults, options));
	},

	//==============================================================================
	// Helper methods for the built-in effects above
	//==============================================================================

	blindWrap: function(proceed){
		var outer = new Element('div').setStyle({ overflow:'hidden' });

		this.element.wrap(outer);
		this.element.show();
		this.element = outer;

		if (proceed) proceed();
	},

	blindUnwrap: function(proceed){
		var inner = this.element.down();

		if (this.element.getHeight() == 0) {
			// We did a blindUp(), so hide before unwrapping 
			// otherwise it'll immediately pop into view again.
			inner.hide();
		}
		
		this.element.replace(inner);
		this.element = inner;

		if (proceed) proceed();
	},

	extendDefaults: function(defaults, options){
		// Extend style objects and wrap callback functions,
		// as opposed to just over-writing those properties.
		if (options) {
			if (defaults.styleFrom && options.styleFrom) options.styleFrom = Object.extend(defaults.styleFrom, options.styleFrom);
			if (defaults.styleTo && options.styleTo) options.styleFrom = Object.extend(defaults.styleTo, options.styleTo);
			if (defaults.onStart && options.onStart) options.onStart = options.onStart.wrap(defaults.onStart);
			if (defaults.onEnd && options.onEnd) options.onEnd = options.onEnd.wrap(defaults.onEnd);
			defaults = Object.extend(defaults, options);
		}
		return defaults;
	},

	getHeightWithMargins: function(el){
		// Be sure that the height includes the margins of any child elements.
		// (A temporary vertical padding or border forces them to be included)
		var p = el.style.paddingBottom;
		if (!p || parseInt(p, 10) == 0) {
			el.setStyle({ paddingBottom:'1px' })
			var h = el.getHeight() - 1;
			el.setStyle({ paddingBottom:p })
		} else {
			var h = el.getHeight();
		}
		return h;
	}

});


//==============================================================================
// Robert Penner's easing functions v2.0 (http://www.robertpenner.com/easing)
// Ported to Scriptaculous 1.8 by Riccardo De Agostini (lozioric AT gmail.com)
//
// Original terms of use (http://www.robertpenner.com/easing_terms_of_use.html)
// also apply to this modification.
//==============================================================================

//==============================================================================
// For the original unmodified source referenced above, see:
// http://snipplr.com/view.php?codeview&id=14458
//------------------------------------------------------------------------------
// For a visualization of these functions see:
// http://hosted.zeh.com.br/tweener/docs/en-us/misc/transitions.html
//==============================================================================
//
// Simple usage example:
//
//    new sense.ui.effect.Tween(el, { transition:'cubicInOut' });
//
// Customization parameters, where present, may be used as follows:
//
//    // No customization (use Penner's default value)
//    new sense.ui.effect.Tween(el, { transition:'backIn' });
//
//    // Customized easing
//    new sense.ui.effect.Tween(el, { transition:sense.ui.effect.easing.back.easeIn.custom(2.5) });
//
//------------------------------------------------------------------------------
// Note that this is a namespace grouping of functions, rather than a class, so 
// it can be referenced statically without instantiating a new Tween() first.
//==============================================================================

sense.ui.effect.easing = (function(){

	//----------------------------------------------------------------------
	// Function transformations
	//----------------------------------------------------------------------

	function reverse(fn,t)			{ return 1 - fn(1 - t); }
	function inToInOut(fn,t)		{ t = 2 * t; return 0.5 * (t < 1 ? fn(t) : 2 - fn(2 - t)); }
	function outToInOut(fn,t)		{ t = 2 * t; return 0.5 * (t < 1 ? 1 - fn(1 - t) : 1 + fn(t - 1)); }
	function pairToInOut(fnA,fnB,t)	{ t = 2 * t; return 0.5 * (t < 1 ? fnA(t) : 1 + fnB(t - 1)); }

	//----------------------------------------------------------------------
	// Function set builders
	//----------------------------------------------------------------------

	function set(fnA, fnB, fnC) {
		return { easeIn:fnA, easeOut:fnB, easeInOut:fnC };
	}
	function setFromIn(fn) {
		return set(fn, reverse.curry(fn), inToInOut.curry(fn));
	}
	function setFromOut(fn) {
		return set(reverse.curry(fn), fn, outToInOut.curry(fn));
	}
	function setFromPair(fnA, fnB) {
		return set(fnA, fnB, pairToInOut.curry(fnA,fnB));
	}

	function customizableSetFromIn() {
		var args = $A(arguments);
		var func = args.shift();

		function customIn() {
			var args = [0].concat($A(arguments));
			return function(t) {
				args[0] = t;
				return func.apply(this, args);
			};
		}

		function customOut() { return reverse.curry(customIn.apply(this, arguments)); }
		function customInOut() { return inToInOut.curry(customIn.apply(this, arguments)); }

		var fnA = customIn.apply(this, args); fnA.custom = customIn;
		var fnB = reverse.curry(fnA); fnB.custom = customOut;
		var fnC = inToInOut.curry(fnA); fnC.custom = customInOut;

		return set(fnA, fnB, fnC);
	}

	//----------------------------------------------------------------------
	// Useful pre-computed values
	//----------------------------------------------------------------------

	var HALF_PI = Math.PI / 2;
	var TWO_PI  = 2 * Math.PI;

	//----------------------------------------------------------------------
	// Core Penner tweening functions, with simplified arguments.
	// Full sets (easeIn, easeOut, easeInOut) are extrapolated from these.
	//----------------------------------------------------------------------

	function sineIn(t)		{ return -1 * Math.cos(t * HALF_PI) + 1; }
	function sineOut(t)		{ return Math.sin(t * HALF_PI); }
	function sineInOut(t)	{ return -0.5 * (Math.cos(Math.PI * t) - 1); }
	function quadIn(t)		{ return t * t; }
	function cubicIn(t)		{ return t * t * t; }
	function quartIn(t)		{ return t * t * t * t; }
	function quintIn(t)		{ return t * t * t * t * t; }
	function powIn(t,p)		{ return Math.pow(t, p); }
	function expoIn(t)		{ return (t == 0) ? 0 : Math.pow(2, 10 * (t - 1)); }
	function expoOut(t)		{ return (t == 1) ? 1 : 1 - Math.pow(2, -10 * t); }
	function circIn(t)  	{ return -1 * (Math.sqrt(1 - t * t) - 1); }
	function circOut(t) 	{ t -= 1; return Math.sqrt(1 - t * t); }
	function backIn(t,s)	{ return t * t * ((s + 1) * t - s); }

	function elasticIn(t,a,p) {
		if (t == 0) return 0;
		if (t == 1) return 1;
		if (a < 1) {
			a = 1;
			var s = p / 4;
		} else {
			var s = p / TWO_PI * Math.asin(1 / a);
		}
		return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * TWO_PI / p));
	}

	function bounceOut(t) {
		if (t < (1 / 2.75)) return 7.5625 * t * t;
		if (t < (2 / 2.75)) return 7.5625 * (t -= (1.5 / 2.75)) * t + 0.75;
		if (t < (2.5 / 2.75)) return 7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375;
		return 7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375;
	}

	//--------------------------------------------------------------------------
	// Build and return the equation sets
	//--------------------------------------------------------------------------

	return {
		linear	: function(t){ return t; },
		sine	: set(sineIn, sineOut, sineInOut),
		quad	: setFromIn(quadIn),
		cubic	: setFromIn(cubicIn),
		quart	: setFromIn(quartIn),
		quint	: setFromIn(quintIn),
		pow		: customizableSetFromIn(powIn, 2), // Defaults to quad
		expo	: setFromPair(expoIn, expoOut),
		circ	: setFromPair(circIn, circOut),
		back	: customizableSetFromIn(backIn, 1.70158),
		elastic	: customizableSetFromIn(elasticIn, 1, 0.3),
		bounce	: setFromOut(bounceOut)
	};

})();
