var Fx = {};

/**
 * Fx.Base
 * Note:  This class is intended to be extended by other effects.  Not for actual use.
 * 
 * Parameters:
 * options		object		Holds every optional parameter
 * 
 * options:
 * fps			int			Frames per second
 * duration		int			Length of animation
 * wait			bool		If set to true, it will ignore the requested effect until the current is over.  Defaults to false.
 * onStart		function	Triggers when the effect is started
 * onComplete	function	Triggesr when the effect is over (if aborted or finished).
 */
Fx.Base = new Activa.Class({
	playing: false,
	timer: null,
	time: 0,
	from: 0,
	to: 0,
	options: {
		fps: 50,
		duration: 500,
		wait: false,
		transition: null,
		onStart: function(){},
		onComplete: function(){}
	},
	init: function init(options) {
		this.setOptions(options);
	},	
	setOptions: function setOptions(options) {
		options = options || {};
		for ( var k in this.options ) {
			this.options[k] = (k in options) ? options[k] : this.options[k];
		}
	},
	start: function start(from, to, overrideWait) {
		if ( this.playing && this.options.wait ) {
			return false;
		}
		clearInterval(this.timer);
		this.from = from;
		this.to = to;
		this.time = 0;
		this.startTimer();
		this.transition = this.getTransition();
		return this;
	},
	getTransition: function getTransition() {
		return this.options.transition || Fx.Transitions.Sine.easeInOut;
	},
	frame: function frame() {
		var time = Number(new Date().getTime());
		if ( time < this.time + this.options.duration ) {
			var d = this.transition((time-this.time)/this.options.duration);
			this.set(this.compute(this.from, this.to, d));
		} else {
			clearInterval(this.timer);
			this.set(this.compute(this.from, this.to, 1));
			this.stop();
			this.finished();
		}
	},
	set:function set(value) {
		return value;
	},
	stop:function stop(finished) {
		this.playing = false;
		clearInterval(this.timer);
		this.options.onComplete();
	},
	finished: function finished() {
		return null;
	},
	compute: function compute(from,to,d) {
		return Fx.Base.compute(from, to, d);
	},
	startTimer: function startTimer() {
		this.playing = true;
		this.time = new Date().getTime();
		
		this.timer = setInterval(this.frame.bind(this), (1000 / this.options.fps));
		
		this.options.onStart();
	}
});

Fx.Base.statics({
	compute:function(from, to, d) {
		return (to-from) * d + from;
	}
});

Fx.Transition = new Activa.Class({
	init: function init(transition, params) {
		transition.easeIn = function(pos){
			return transition(pos, params);
		};
		transition.easeOut = function(pos) {
			return 1 - transition(1 - pos, params);
		};
		transition.easeInOut = function(pos) {
			return (pos <= 0.5) ? transition(2 * pos, params) / 2 : (2 - transition(2 * (1 - pos), params)) / 2;
		};
		return transition;
	}
})


Fx.Transitions = {
	Sine: function trans(p) {
		return 1 - Math.sin((1 - p) * Math.PI / 2);
	},
	Circ: function circ(p){
		return 1 - Math.sin(Math.acos(p));
	},
	Expo: function expo(p){
		return Math.pow(2, 8 * (p - 1));
	},
	Bounce: function(p){
		var value;
		for (var a = 0, b = 1; 1; a += b, b /= 2){
			if (p >= (7 - 4 * a) / 11){
				value = b * b - Math.pow((11 - 6 * a - 11 * p) / 4, 2);
				break;
			}
		}
		return value;
	}
};

for ( var i in Fx.Transitions ) { 
	Fx.Transitions[i] = new Fx.Transition(Fx.Transitions[i]); 
};


/**
 * Fx.Style
 * 
 * Parameters:
 * elm			element			Element to apply the effect on.
 * options		object			Object to hold optional parameters.
 * 
 * options (Inherits Fx.Base's options):
 * unit			string			Unit used by the style. Ex: px, %, pt
 */
Fx.Style = new Activa.Class({
	Extends:Fx.Base,
	options:{
		unit:null
	},
	init:function init(elm, options){
		this.elm = elm;
		this.unit = '';
		this.parser = {};
		this.uniquid = (new Date().getTime()*(Math.round(Math.random() * 100)));
		this.root(options);
	},
	set: function set(value){
		if( typeof value == 'object' ) {
			for ( var k in value ) {
				this.setStyle(k, value[k]);
			}
			return;
		}
		this.setStyle(value);
	},
	compute: function compute(from, to, d) {
		if ( typeof from == 'object' ) {
			var values = {};
			for ( k in from ) {	
				values[k] = this.parser[to[k]].call(this, from[k], to[k], d);
			}
			return values;
		}
		
		return this.parser[to].call(this, from, to, d);
	},
	start: function start(style, from, to) {
		
		if( typeof style == 'object' ) {
			var from = {}, to = {}, parser = {};
			for ( k in style ) {
				to[k] = style[k];
				this.setParser(to[k]);
				from[k] = this.getStyle(k);
			}
		}

		if ( typeof style != 'object' && !to ) {
			this.style = style;
			var to = from;
			var from = this.getStyle();
			this.getUnit(this.style);
			this.setParser(to);
		}
				
		this.root(from, to);
	},
	setParser: function getParser(value) {
		for ( var parser in Fx.Style.Parsers ) {
			if ( Fx.Style.Parsers[parser].isParser(value) ) {
				this.parser[value] = Fx.Style.Parsers[parser].compute;
				return;
			}
		}
		this.parser[value] = Fx.Base.compute;
	},
	getStyle:function STgetStyle(style){
		style = style || this.style;
		if ( !window.ActiveXObject ) {
			style = Fx.Style.cssStyle(style);
		}
		
		if ( style == 'opacity' ) {
			return this.getOpacity();
		}
		
		var result = getStyle(this.elm, style);
		if ( Fx.Style.Parsers.Color.isParser(result) || style == 'position' ) {
			return result;
		}
		return isNaN(parseInt(result)) ? 0 : Number(result);
	},
	setStyle:function setStyle(style, value){

		if ( typeof value == 'undefined' ) {
			var value = style;
			style = this.style;
		}
		
		if(!this.unit) {
			this.getUnit(style);
		}
		
		if(style == 'opacity'){
			return this.setOpacity(value);
		}
		this.elm.style[style] = value + this.unit;
	},
	getOpacity:function getOpacity(){
		if ( typeof Fx.Style.opacities[this.uniquid] != 'undefined' ) {
			return Fx.Style.opacities[this.uniquid];
		}
		var style = getStyle(this.elm, 'opacity');
		return style || 1;
	},
	setOpacity:function setOpacity(value) {
		value = Math.abs(value);
		if ( window.ActiveXObject ) {
			this.elm.style.filter = 'alpha(opacity = '+Math.round(value*100)+')';
			this.elm.style.zoom = '1';
		} else {
			this.elm.style.opacity = Math.round(value*100)/100;
		}
		Fx.Style.opacities[this.uniquid]=value;
	},
	getUnit:function getUnit(style){
		if ( /color/i.test(style) ) {
			return this.unit = '';
		};
		switch(style){
			default:
				this.unit = this.options.unit;
			break;
		};
		return this.unit;
	}
});

Fx.Style.statics({
	opacities: {},
	cssStyle: function(style) {
		if ( window.ActiveXObject ) { return this.jsStyle(style); };
		return style.replace(/([A-Z])/g,function(a) { return '-'+a; }).toLowerCase();
	},
	jsStyle: function(style) {
		return style.replace(/(-[a-z])/g,function(a) { return a.replace('-', '').toUpperCase(); });
	},
	Parsers: {
		Color: {
			isParser: function isColor(string) {
				return /^(#([a-f0-9]{3,6})|rgb\((\d+),\s(\d+),\s(\d+)\))$/i.test(String(string));
			},
			hexToRGB: function hexToRGB(hex) {
				var arr = String(hex).match(/#?(\w{1,2})(\w{1,2})(\w{1,2})/i);
				if ( !arr ) {
					return [255,255,255];
				}
				arr.shift();
				return arr.map(function hexMap(a) {
					if ( a.length == 1 ) {
						a += a;
					}
					return parseInt(a, 16);
				});
			},
			compute: function colorCompute(from, to, d) {
				from = Fx.Style.Parsers.Color.hexToRGB(from); to = Fx.Style.Parsers.Color.hexToRGB(to);
				return '#'+rgbToHex.apply(null,[0, 1, 2].map(function map(i){
					return Fx.Base.compute(from[i], to[i], d)
				}));
			}
		}
	}
});

/**
 * Fx.Fade
 * 
 * Parameters:
 * elm			element		The HTML node to perform the fade on
 * options		object		An object to hold optional values
 * 
 * Options (Inherited from Fx.Base and Fx.Style)
 * 
 * Example Usage:
 * new Fx.Fade(did('divFade'), { duration: 800, onComplete: function(){ alert('Done!'); } }).fadeOut();
 */
Fx.Fade = new Activa.Class({
	Extends: Fx.Style,
	init:function init(elm,options){
		this.root(elm,options);//parent call
		this.style = 'opacity';
	},
	start: function start() {
		var args = Activa.toArray(arguments);
		args.unshift('opacity');
		this.root.apply(this,args);
	},
	hide: function hide() {
		return this.set(0);
	},
	show: function show() {
		return this.set(1);
	},	
	fadeIn: function fadeIn() {
		return this.start(1);
	},
	fadeOut: function fadeOut() {
		return this.start(0);
	},
	toggle: function toggle() {
		if ( this.getOpacity() == 1 ) {
			this.inout = 'out';
			this.fadeOut();
		}else{
			this.inout = 'in';
			this.fadeIn();
		}
	}
});	

/**
 * Fx.Slide
 *  
 * Parameters:
 * elm			element		A HTML node to perform the effect on.
 * options		object		An object to hold the optional values.
 * 
 * options (Inherits Fx.Base options and Fx.Style):
 * mode			string		Whether to slide horizontally or vertically.  Accepts 'horizontal' or 'vertical'(default).
 * collapse		bool		If set to true the element will actually collapse during the fade.  If false, the space of the element is maintained.  Defaults to false.
 * 
 * Example usage:
 * var slide = new Fx.Slide(did('elm'), { mode: 'horizontal', duration: 5000 });
 * 		slide.slideIn(); 
 */
Fx.Slide = new Activa.Class({
	Extends: Fx.Style,
	options: {
		mode: 'vertical',
		collapse: false
	},
	set: function set(value) {
		this.elm.style[this.style] = value[0]+'px';
		if ( this.options.collapse ) {
			this.wrapper.style[this.layout] = value[1]+'px';
		}
	},
	compute: function compute(from, to, d ) {
		return [0,1].map(function computeMap(i) {
			return Fx.Base.compute(from[i], to[i], d);
		});
	},
	init: function init(elm, options) {
		this.wrapper = document.createElement('div');		
		this.wrapper.style.overflow = 'hidden';
		this.wrapper.className = 'wrapper';
		this.root(elm, options);
		this.container = this.elm.parentNode;
		if ( !Activa.DOM.hasClass(this.container, 'wrapper') ) { 
			this.container.insertBefore(this.wrapper, this.elm);
			this.wrapper.appendChild(this.elm);
		} else {
			this.wrapper = this.container;
			this.container = this.wrapper.parentNode;
		}
		this.container.style.position = 'static';
		
		switch ( this.options.mode ) {
			case 'vertical':
				this.type = 'offsetHeight';
				this.style = 'marginTop';
				this.layout = 'height';
			break;
			case 'horizontal':
				this.type = 'offsetWidth';
				this.style = 'marginLeft';
				this.layout = 'width';
			break;
		};
		this.sizeBox();
	},
	sizeBox: function sizeBox() {
		this.elm.style.display = 'block';
		this.wrapper.style.width = this.elm.offsetWidth+'px';
		this.wrapper.style.height = this.elm.offsetHeight+'px';
	},
	start: function start(direction) {
		this.sizeBox();
							
		var margin = parseInt( getStyle(this.elm, Fx.Style.cssStyle(this.style)) ),
			layout = parseInt( getStyle(this.elm, Fx.Style.cssStyle(this.layout)) );
		
		if ( isNaN(margin) ) {
			margin = 0;
		}
	
		if ( direction == 'toggle' ) {
			direction = margin == 0 ? 'out' : 'in';
		}
		
		var starting = this.elm[this.type];
		
		var which = direction == 'out' ? [-starting, 0] : [0, starting];
									
		var from = [margin, layout];
		this.root(false, from, which);
	},
	slideIn: function slideIn() {
		this.start('in');
	},
	slideOut: function slideOut() {
		this.start('out');
	},
	show: function show() {
		this.set(this.compute([0,0],[0,0],1));
	},
	hide: function hide() {
		var finished = -this.elm[this.type];
		this.set(this.compute([0,0],[finished,finished],1));
	},
	toggle: function toggle() {
		this.start('toggle');
	}
});

/**
 * Fx.Move
 * Requires an element to be absolutely or relatively positioned.  If neither, it forces absolute.
 * 
 * Parameters:
 * elm				element		Element to move
 * options			object		Options object.  Inherited from Style and Base
 * 
 * Example Usage:
 * new Fx.Move(did('element'), { duration: 5000 }).start({ x: 400, y: 300 });
 * new Fx.Move(did('element'), { duration: 5000 }).start({ y: 150 });
 */
Fx.Move = new Activa.Class({
	Extends: Fx.Style,
	start: function start(x, y) {
		if ( !['relative','absolute'].inArray(this.getStyle('position')) ){
			this.elm.style.position = 'absolute';
		}
		
		var coords = { x: x, y: y };
		if ( typeof x == 'object' ) {
			var coords = x;
		}
		
		this.root({ left: coords.x, top: coords.y });
	}
});

/**
 * Fx.Scroll
 * 
 * Parameters:
 * elm				element		Element to scroll.  Defaults to window.
 * options			object		Options object.  Inherited from Style and Base
 * 
 * Example Usage:
 * new Fx.Scroll().start(0,100);//Scrolls down the entire page to 100
 * new Fx.Scroll(did('element')).start(0,100); //Scrolls a specific element down to 100
 */

Fx.Scroll = new Activa.Class({
	Extends: Fx.Base,
	init: function init(elm, options) {
		this.elm = elm || window;
		this.root(options);
	},
	set: function set(value) {
		if ( this.elm == window ) {
			return window.scrollTo(value[0], value[1]);
		}
		this.elm.scrollLeft = value[0];
		this.elm.scrollTop = value[1];
	},
	compute: function compute(from, to, d) {
		return [0,1].map(function computeMap(i) {
			return Fx.Base.compute(from[i], to[i], d);
		});
	},
	start: function start(x, y) {
		if ( this.elm == window ) {
			var s = Activa.Dimensions.getScrollXY();
		} else {
			var s = { x: this.elm.scrollLeft, y: this.elm.scrollTop };
		}
		
		return this.root([s.x, s.y], [x, y]);
	},
	toTop: function toTop() {
		this.start(0, 0);
	},
	toBottom: function toBottom() {
		this.start(0, (this.elm.scrollHeight - this.elm.offsetHeight));
	}
});
