/**
 *
 * Phi Core - A multi-paradigm JavaScript library
 *
 *
 *
 * Copyright (c) 2010 Arjen Geerse, Peter Nederlof, Olivier Hermanus
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 *
 */

(function() {
	
	/**
	 *
	 * Public.
	 *
	 *
	 */
	
	phi.anim = {
		BasicAnimation: function(handler, params) {
			return new BasicAnimation(handler, params);
		},
		CSSAnimation: function(from, to, params) {
			return new CSSAnimation(from, to, params);
		},
		easing: {
			LINEAR: function (t) {
				return t;
			},
			/* simple ease */
			EASEINQUAD: function(x, t, b, c, d) {
				return c * (t /= d) * t + b;
			},
			EASEOUTQUAD: function() {},
			EASEINOUTQUAD: function() {},
			
			/* exponential ease */
			EXPOIN: function() {},
			EXPOOUT: function() {},
			EXPOINOUT: function() {},
			
			/* bouncing ease */
			BOUNCEIN: function() {},
			BOUNCEOUT: function() {},
			BOUNCEINOUT: function() {},
			
			/* elastic ease */
			ELASTICIN: function() {},
			ELASTICOUT: function() {},
			ELASTICINOUT: function() {}
		},
		getAnimator: function() {
			if (!_animator) {
				_animator = new Animator();
			}
			return _animator;
		},
		finish: function (node) {
			_animator.finish(node);
		}
	};

	/** 
	 *
	 * BasicAnimation
	 *
	 */
	var BasicAnimation = new Class({
		duration:500,
		delay:0,
		callback: null,
		easing: phi.anim.easing.LINEAR,
		__init: function(handler, params) {
			this.handler = handler;
			for (var i in params) {
				this[i] = params[i];
			}
			this.running = false;
		},
	
		start: function () {
			if (!this.running) {
				this.running = true;
				this.id = phi.anim.getAnimator().start(this);
			}
		},
		
		handleFrame: function(t) {
			this.handler(t);
		},
		
		handleEnd: function(reversed) {
			this.running = false;
			if (this.callback) this.callback(reversed);
		}
	});

	/** 
	 *
	 * CSSAnimation
	 *
	 */
	var CSSAnimation = new Class({
		duration:500,
		delay:0,
		callback: null,
		easing: phi.anim.easing.LINEAR,
		__init: function(from, to, params) {
			this.props = convertProperties(from,to);
			for (var i in params) {
				this[i] = params[i];
			}
			this.running = false;
		},
	
		start: function (node, override) {
			if (!this.running) {
				this.params = {
					duration:this.duration,
					delay:this.delay,
					callback: this.callback,
					easing: this.easing
				};
				for (var i in override) {
					this[i] = override[i];
				}
				this.running = true;
				this.node = node;
				this.id = phi.anim.getAnimator().start(this);
			}
		},
		
		handleFrame: function(t) {
			var style = this.node.style;
			for (var i in this.props) {
				var prop = this.props[i];
				switch (prop.name) {
					case 'opacity':
						if (document.all) {
							style.filter = 'alpha(opacity='+Math.round((prop.from + prop.delta*t)*100)+')';
							break;
						}
					default:
						if (prop.float) {
							style[prop.name] = (prop.from + prop.delta*t) + prop.unit;
						} else {
							style[prop.name] = Math.round(prop.from + prop.delta*t) + prop.unit;
						}
					break;
				}
			}
		},
		
		handleEnd: function(reversed) {
			for (var i in this.params) {
				this[i] = this.params[i];
			}
			this.running = false;
			if (this.callback) this.callback(reversed);
		}
	});

	/** 
	 *
	 * Property helper
	 *
	 */
	function convertProperties(from, to) {
		var props = new Array();
		var format = new RegExp('^([0-9.-]+)(em|px|%)$');
		for (var i in from) {
			if (format.test(from[i])) {
				var froms = format.exec(from[i]);
				var tos = format.exec(to[i]);
				var prop = {
					name: String(i),
					from: parseFloat(froms[1]),
					delta: parseFloat(tos[1]) - parseFloat(froms[1]),
					unit: froms[2] || ''
					
				};
			} else {
				var prop = {
					name: String(i),
					from: parseFloat(from[i]),
					delta: parseFloat(to[i]) - parseFloat(from[i]),
					unit: ''
					
				};
			}
			prop.float = (prop.unit === 'em' || prop.name === 'opacity') ? true : false;
			props.push(prop);
		}
		return props;
	}

	/**
	 *
	 * Animator class
	 *
	 */
	var _animator = null;
	var Animator = new Class({
		__init: function() {

			this.unique = 1;//Maintain id's for animation
			this.queue = new Array();//Queue of active animations
			this.running = false;//Timeout ID or false
		},
		start: function (animation) {
			//Tag with time and id and push into queue
			var ao = {
				animation:animation,
				id:this.unique++,
				finished:false,
				tStart: animation.delay + new Date().getTime()
			};
			this.queue.push(ao);
			
			if (!this.running) {
				this.running = setTimeout(this.process.bind(this), 25);
			}
			
			return ao.id;
		},
		finish: function (node) {
			for (var i=0, il=this.queue.length; i < il; i++) {
				var anim = this.queue[i].animation;
				if (node === anim.node) {
					this.queue[i].finished = true;
				}
			}		
		},
		process: function() {
			var stopped = new Array();
			var tNow = new Date().getTime();
			
			for (var i=0, il=this.queue.length; i < il; i++) {
				var tStart = this.queue[i].tStart;
				var item = this.queue[i];
				var anim = item.animation;
				
				if (tNow - tStart < 0 && !item.finished) continue;//Hold delayed anim's
				
				var tAnim = (tNow - tStart) / anim.duration;//Determine relative time
				if (tAnim < 1 && item.finished === false) {
					if (anim.reversed) {
						anim.handleFrame.call(anim, anim.easing(1 - tAnim));
					} else {
						anim.handleFrame.call(anim, anim.easing(tAnim));
					}
				} else {//Stop animation at round values
					if (anim.reversed) {
						anim.handleFrame.call(anim, anim.easing(0));
					} else {
						anim.handleFrame.call(anim, anim.easing(1));
					}
					stopped.push(this.queue[i]);
				}
			}
			
			//Remove stopped animations
			if (stopped.length > 0) {
				var newQueue = new Array;
				for (i=0, il = this.queue.length; i < il; i++) {
					if (stopped.indexOf(this.queue[i]) === -1) {
						newQueue.push(this.queue[i]);
					}
				}
				this.queue = newQueue;
			}
			//Perform callbacks
			for (var i=0, il=stopped.length; i < il; i++) {
				var anim = stopped[i].animation;
				anim.handleEnd.call(anim, anim.reversed);
			}
			//Keep the motor running
			if (this.queue.length > 0) {
				this.running = setTimeout(this.process.bind(this), 25);
			} else {
				this.running = false;
			}
		}

	});
	
	
})();
