var Effect = 
{
	Transitions: {
		linear: Prototype.K,
		
		sinoidal: function(pos)
		{
			return (-Math.cos(pos * Math.PI) / 2) + 0.5;
		}
	},
	
	DefaultOptions:
	{
		duration: 0.5,
		fps: 100,
		sync: false,
		from: 0.0,
		to: 1.0,
		delay: 0.0,
		queue: 'parallel'
	}
};

Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;

Effect.ScopedQueue = Class.create(Enumerable,
{
	initialize: function()
	{
		this.effects  = [];
		this.interval = null;
	},
		
	add: function(effect)
	{
		var timestamp = new Date().getTime();
		
		var position = Object.isString(effect.options.queue) ? effect.options.queue : effect.options.queue.position;
		
		switch(position)
		{
			case 'front':
				
			this.effects.findAll(function(e){ return e.state=='idle' }).each(function(e)
			{
				e.startOn  += effect.finishOn;
				e.finishOn += effect.finishOn;
			});
			
			break;
		}
    
		effect.startOn  += timestamp;
		effect.finishOn += timestamp;

		if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
			this.effects.push(effect);
    
		if(!this.interval)
			this.interval = setInterval(this.loop.bind(this), 15);
	},
		
	remove: function(effect)
	{
		this.effects = this.effects.reject(function(e) { return e==effect });

		if(this.effects.length == 0)
		{
			clearInterval(this.interval);
			this.interval = null;
		}
	},
		
	loop: function()
	{
		var timePos = new Date().getTime();

		for(var i=0, len=this.effects.length; i < len; i++) 
			this.effects[i] && this.effects[i].loop(timePos);
	}
});

Effect.Queues = 
{
	instances: $H(),
	get: function(queueName)
	{
		if(!Object.isString(queueName)) return queueName;
			return this.instances.get(queueName) || this.instances.set(queueName, new Effect.ScopedQueue());
	}
};

Effect.Base = Class.create(
{
	position: null,
	
	start: function(options)
	{
		function codeForEvent(options,eventName)
		{
			return ((options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') + 
					(options[eventName] ? 'this.options.'+eventName+'(this);' : ''));
		}
		
		if(options && options.transition === false) options.transition = Effect.Transitions.linear;
			this.options = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
			this.currentFrame = 0;
			this.state = 'idle';
			this.startOn = this.options.delay * 1000;
			this.finishOn = this.startOn + (this.options.duration * 1000);
			this.fromToDelta = this.options.to-this.options.from;
			this.totalTime = this.finishOn-this.startOn;
			this.totalFrames = this.options.fps * this.options.duration;
			
		eval('this.render = function(pos){ ' + 
			'if(this.state=="idle"){this.state="running";' + 
			codeForEvent(this.options,'beforeSetup') + 
			(this.setup ? 'this.setup();':'') + 
			codeForEvent(this.options,'afterSetup') + 
			'};if(this.state=="running"){' + 
			'pos=this.options.transition(pos)*'+this.fromToDelta+'+'+this.options.from+';' + 
			'this.position=pos;' + 
			codeForEvent(this.options,'beforeUpdate') + 
			(this.update ? 'this.update(pos);':'') + 
			codeForEvent(this.options,'afterUpdate') + 
			'}}');
		
		this.event('beforeStart');
		
		if(!this.options.sync)
			Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).add(this);
	},
		
	loop: function(timePos)
	{
		if(timePos >= this.startOn)
		{
			if(timePos >= this.finishOn)
			{
				this.render(1.0);
				this.cancel();
				this.event('beforeFinish');

				if(this.finish) this.finish();
					this.event('afterFinish');
					return;  
			}
			
			var pos = (timePos - this.startOn) / this.totalTime, frame = (pos * this.totalFrames).round();
			
			if(frame > this.currentFrame)
			{
				this.render(pos);
				this.currentFrame = frame;
			}
		}
	},
		
	cancel: function()
	{
		if(!this.options.sync)
			Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).remove(this);
		
		this.state = 'finished';
	},
		
	event: function(eventName)
	{
		if(this.options[eventName + 'Internal'])
			this.options[eventName + 'Internal'](this);

		if(this.options[eventName])
			this.options[eventName](this);
	}
});

Effect.Tween = Class.create(Effect.Base,
{
	initialize: function(object, from, to)
	{
		object = Object.isString(object) ? $(object) : object;

		var args = $A(arguments), method = args.last(), options = args.length == 5 ? args[3] : null;
		
		this.method = Object.isFunction(method) ? method.bind(object) :
		
		Object.isFunction(object[method]) ? object[method].bind(object) :
		
		function(value) { object[method] = value };
		
		this.start(Object.extend({ from: from, to: to }, options || { }));
	},
	
	update: function(position)
	{
		this.method(position);
	}
});

Effect.ScrollTo = function(element)
{
	var options = arguments[1] || { }, scrollOffsets = document.viewport.getScrollOffsets(), elementOffsets = $(element).cumulativeOffset(), 
	max = (window.height || document.body.scrollHeight) - document.viewport.getHeight();  
	
	if(options.offset)
		elementOffsets[1] += options.offset;
	
	return new Effect.Tween(null, scrollOffsets.top, elementOffsets[1] > max ? max : elementOffsets[1], options, 

	function(p)
	{
		scrollTo(scrollOffsets.left, p.round())
	}
	);
};
