var Transitions = {
	linear: function(p) {
		return p;
	},
	sineInOut: function(p) {
		return (Math.sin((p * Math.PI) - (Math.PI / 2)) + 1) / 2;
	},
	sineOut: function(p) {
		return 1 - Math.sin((1 - p) * Math.PI / 2);
	},
	cubicOut: function(p) {
		return Math.pow(p - 1, 3) + 1;
	},
	quintOut: function(p) {
		return Math.pow(p - 1, 5) + 1;
	},
	back: function(p){
		return Math.pow(p, .8) * (2.618 * p - 1.618);
	},
	circ: function(p) {
	    if (p < .52) {
	        return Math.sin(p / 2 * Math.PI) * 1.6
	    } else if (p >= .52 && p < .675) {
	        return -(p * 2) + 2.2;
	    } else if (p >= .675 && p < .78) {
	        return (p * 2) - .5;
	    } else if (p >= .78 && p < .92) {
	        return -p + 1.835;
	    } else {
	        return p;
	    }
	}
};

var css3transformTitle = (function() {
    if (Browser.Engine.webkit) {
        return '-webkit-transform';
    } else if (Browser.Engine.gecko) {
        return '-moz-transform';
    } else if (Browser.Engine.presto) {
        return '-o-transform';
    };
    return '-';
})();

var tickFunctions = {
    css: function(el, styles) {
        var css3transformString = '';
		for (var i in styles) {
			if (i == 'scale') {
				css3transformString += ' scale(' + styles[i] + ')';
				delete styles[i];
			} else if (i == 'rotate') {
				css3transformString += ' rotate(' + styles[i] + 'deg)';
				delete styles[i];
			}
		};
		styles[css3transformTitle] = css3transformString;
		el.setStyles(styles);
    }
};

var Tween = new Class({
	Implements : [Events, Chain, Options],
	options : {
		onStart    : $empty,
		onComplete : $empty,
		fps        : 50,
		position   : 'absolute'
	},
	duration       : 0,
	animation      : {},
	elements       : [],
	time           : 0,
	/**
	 * @param {Element[]} elms
	 * @return {Object}
	 */
	initialize : function(elms, options) {
		this.setOptions(options);
		
		if ($type.array(elms)) {
			this.elements = elms;
		}
		if ($type.string(elms)) {
			this.elements = $$(elms);
		}
		$each(this.elements, function(elm) {
			if (elm.getStyle('position') == 'static') {
				elm.setStyle('position', this.options.position);
			}
		}, this);
		
		return this;
	},
	/**
	 * @return {Object}
	 * obj = {
	 *     property : 'height',
	 *     values   : [],
	 *     duration : 250
	 *     offset   : 0
	 * }
	 */
	start : function(obj) {
		this.prepareAnimation(obj);
		this.fireEvent('start');
		this.startTimer();
		return this;
	},
	startTimer : function() {
		if (this.timer) {
			return false;
		}
		this.time = $time();
		this.timer = this.step.periodical(Math.round(1000 / this.options.fps), this);
		return true;
	},
	stopTimer : function() {
		if (!this.timer) {
			return false;
		}
		this.time = $time() - this.time;
		this.timer = $clear(this.timer);
		return true;
	},
	step : function() {
		var time = $time();
		var delta;
		if (time < this.time + this.duration) {
			for (var i in this.animation) {
				if (time >= this.time + this.animation[i].offset && time < this.time + this.animation[i].total) {
					delta = this.transition((time - (this.time + this.animation[i].offset)) / this.animation[i].duration);
					this.set(i, this.animation[i].property, this.compute(this.animation[i].values, delta));
				}
				if (time >= this.time + this.animation[i].total) {
					this.set(i, this.animation[i].property, this.compute(this.animation[i].values, 1));
				}
			}
		} else {
			for (var i in this.animation) {
				this.set(i, this.animation[i].property, this.compute(this.animation[i].values, 1));
			}
			this.complete();
		}
	},
	complete : function() {
		if (this.stopTimer()) {
			this.animation = {};
			this.fireEvent('complete');
			this.callChain.delay(100, this);
		}
		return this;
	},
	transition : function(p) {
		return (Math.sin((p * Math.PI) - (Math.PI / 2)) + 1) / 2;
	},
	compute : function(obj, delta) {
		return (obj[1] - obj[0]) * delta + obj[0];
	},
	set : function(elm, property, n) {
		this.elements[elm].setStyle(property, n);
	},
	prepareAnimation : function(obj) {
		for (var i in obj) {
			this.animation[i] = obj[i];
			
			if (!$type.array(this.animation[i].values)) {
				if (window.getComputedStyle) {
			        this.animation[i].values = [this.elements[i].getComputedStyle(this.animation[i].property).toFloat(), this.animation[i].values];
			    } else {
			        this.animation[i].values = [this.elements[i].getStyle(this.animation[i].property).toFloat(), this.animation[i].values];
			    };
			};
			
			if (!this.animation[i]['offset']) {
				this.animation[i]['offset'] = 0;
			}
			
			this.animation[i].total = this.animation[i].duration + this.animation[i].offset;
			
			if (this.animation[i].total > this.duration) {
				this.duration = this.animation[i].duration + this.animation[i].offset;
			}
		};
	}
});

var Morph = new Class({
	Implements: [Events, Chain, Options],
	options : {
		onStart    : $empty,
		onComplete : $empty,
		fps        : 50,
		position   : 'absolute',
		duration   : 500
	},
	element        : null,
	config         : {},
	time           : 0,
	values         : {},
	initialize: function(element, options) {
		this.setOptions(options);
		this.element = $(element);
		if (this.element.getStyle('position') == 'static') {
			this.element.setStyle('position', this.options.position);
		}
		return this;
	},
	/*
	 * config = {
	 *     cssProperty: {
	 *         values: Number|Number[]
	 *         transition: Function
	 *     },
	 *     ...
	 * };
	 */
	start: function(config) {
		this.prepareConfig(config);
		this.fireEvent('start');
		this.startTimer();
		return this;
	},
	startTimer : function() {
		if (this.timer) {
			return false;
		}
		this.time = $time();
		this.timer = this.step.periodical(Math.round(1000 / this.options.fps), this);
		return true;
	},
	step: function() {
		var time = $time();
		var delta;
		
		if (time < this.time + this.options.duration) {
			for (var cssProperty in this.config) {
				delta = this.transition((time - this.time) / this.options.duration, this.config[cssProperty].transition);
				this.values[cssProperty] = this.compute(this.config[cssProperty].values, delta);
			}
			this.set(this.values);
		} else {
			for (var cssProperty in this.config) {
				this.values[cssProperty] = this.compute(this.config[cssProperty].values, 1);
			}
			this.set(this.values);
			this.complete();
		}
	},
	set: function(values) {
		this.element.setStyles(values);
	},
	transition: function(progress, func) {
		return func(progress);
	},
	compute : function(obj, delta) {
		return (obj[1] - obj[0]) * delta + obj[0];
	},
	stopTimer : function() {
		if (!this.timer) {
			return false;
		}
		this.time = $time() - this.time;
		this.timer = $clear(this.timer);
		return true;
	},
	complete : function() {alert('a');
		if (this.stopTimer()) {
			this.config = {};
			this.fireEvent('complete');
			this.callChain.delay(100, this);
		}
		return this;
	},
	prepareConfig: function(config) {
		this.config = {};
		
		for (var cssProperty in config) {
			this.values[cssProperty] = 0;
			if ($type.number(config[cssProperty].values)) {
				config[cssProperty].values = [this.element.getComputedStyle(cssProperty).toFloat(), config[cssProperty].values];
			};
		};
		
		this.config = config;
	}
});

var Timeline = new Class({
    Implements: [Options, Chain, Events],
    
    options: {
        onStart: $empty,
        onComplete: $empty,
        fps: 30
    },
    
    duration: 0,
    animation: [],
    animationLength: 0,
    elements: [],
    time: 0,
    timer: null,
    loop: false,
    doneAnimations: [],
    
    initialize: function(options) {
        this.setOptions(options);
    },
    
    /**
     * fx.setTimeline({
     *     element: любой объект, включая DOM-элемент;
     *     tick: функция, выставляющая значения элемену (по умолчанию делает setStyles);
     *     times: {
     *         time1: {
     *             property1: {
     *                 values: массив или число,
     *                 transition: функция
     *                 duration: число миллисекнд
     *             },
     *             property2: {
     *                 ...
     *             }
     *         },
     *         time2: {
     *             ...
     *         }
     *     }
     * }, {
     *     ...
     * }, {
     *     ...
     * });
     */
    setTimeline: function() {
        this.animation = Array.prototype.slice.call(arguments);
        this.animationLength = this.animation.length;
        this.calculateDuration();
        return this;
    },
    
    setLoop: function(bool) {
        this.loop = (!arguments.length || !bool) ? false : true;
    },
    
    calculateDuration: function() {
        this.duration = 0;
        for (var i = 0; i < this.animation.length; i++) {
            for (var j in this.animation[i].times) {
                j = j.toInt();
                for (var n in this.animation[i].times[j]) {
                    if ((j + this.animation[i].times[j][n].duration) > this.duration) {
                        this.duration = j + this.animation[i].times[j][n].duration;
                    };
                };
            };
        };
        return this;
    },
    
    start: function() {
        this.fireEvent('start');
		this.startTimer();
		return this;
    },
    
    startTimer : function() {
		if (this.timer) {
			return false;
		}
		this.time = $time();
		this.timer = this.step.periodical(Math.round(1000 / this.options.fps), this);
		return true;
	},
	
	stopTimer : function() {
		if (!this.timer) {
			return false;
		}
		this.time = $time() - this.time;
		this.timer = $clear(this.timer);
		return true;
	},
	
	step: function() {
	    var t = $time();
	    var values = {};
	    var delta;
	    
	    if (t < this.time + this.duration) {
	        for (var i = 0; i < this.animationLength; i++) {
	            values = {};
	            for (var time in this.animation[i].times) {
	                time = time.toInt();
	                if (t >= time + this.time) {
	                    for (var css in this.animation[i].times[time]) {
	                        if (!this.animation[i].times[time][css].done) {
	                            if (t < (this.animation[i].times[time][css].duration + time + this.time)) {
                                    delta = this.transition((t - (this.time + time)) / this.animation[i].times[time][css].duration, this.animation[i].times[time][css].transition);
    	                        } else {
    	                            delta = 1;
                                    this.animation[i].times[time][css]['done'] = true;
                                    this.doneAnimations.push(this.animation[i].times[time][css]);
    	                        }
    	                        values[css] = this.compute(this.animation[i].times[time][css].values, delta);
	                        };
	                    };
	                };
	            };
	            this.animation[i].tick(this.animation[i].element, values);
	        };
        } else {
            for (var i = 0; i < this.animationLength; i++) {
                values = {};
                for (var time in this.animation[i].times) {
	                time = time.toInt();
	                if (t >= time + this.time) {
	                    for (var css in this.animation[i].times[time]) {
	                        if (!this.animation[i].times[time][css].done) {
	                            values[css] = this.compute(this.animation[i].times[time][css].values, 1);
                            };
                        };
                    };
                };
                this.animation[i].tick(this.animation[i].element, values);
            };
            if (this.loop) {
                this.restart();
            } else{
                this.complete();
            };
        };
	},
	
	restart: function() {
	    for (var i = 0; i < this.doneAnimations.length; i++) {
	        this.doneAnimations[i]['done'] = false;
	    };
	    this.time = $time();
	    return this;
	},
	
	transition: function(progress, func) {
		return func(progress);
	},
	
	compute : function(obj, delta) {
		return (obj[1] - obj[0]) * delta + obj[0];
	},
	
	complete : function() {
		if (this.stopTimer()) {
			this.animation = {};
			this.fireEvent('complete');
			this.callChain.delay(100, this);
		}
		return this;
	}
});
