var Effect = 
{
	Transitions:
	{
		linear: Prototype.K,
		sinoidal: function(pos)
		{
			return (-Math.cos(pos*Math.PI)/2) + .5;
		}
	},
		
	DefaultOptions:
	{
		duration: 0.5,
			fps: 100,
			sync: false,
			from: 0.0,
			to: 1.0,
			delay: 0.0,
			queue: 'parallel'
	},
		
	tagifyText: function(element)
	{
		var tagifyStyle = 'position:relative';
		
		if (Prototype.Browser.IE)
			tagifyStyle += ';zoom:1';
		
		element = $(element);
		
		$A(element.childNodes).each( function(child)
		{
			if (child.nodeType==3)
			{
				child.nodeValue.toArray().each( function(character)
				{
					element.insertBefore(new Element('span', {style: tagifyStyle}).update(character == ' ' ? String.fromCharCode(160) : character),child);
				});
				
				Element.remove(child);
			}
		});
	}
};

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;
			
			case 'with-last':
				timestamp = this.effects.pluck('startOn').max() || timestamp;
			break;
			
			case 'end':
				timestamp = this.effects.pluck('finishOn').max() || timestamp;
			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.Queue = Effect.Queues.get('global');

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;
		
		this.render = (function()
		{
			function dispatch(effect, eventName)
			{
				if (effect.options[eventName + 'Internal'])
					effect.options[eventName + 'Internal'](effect);
				if (effect.options[eventName])
					effect.options[eventName](effect);
			}
			
			return function(pos)
			{
				if (this.state === "idle")
				{
					this.state = "running";
					dispatch(this, 'beforeSetup');
					if (this.setup)
						this.setup();
					dispatch(this, 'afterSetup');
				}
				if (this.state === "running")
				{
					pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
					this.position = pos;
					dispatch(this, 'beforeUpdate');
					if (this.update)
						this.update(pos);
					dispatch(this, '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();
	
	if (options.offset)
		elementOffsets[1] += options.offset;
	
	return new Effect.Tween(null, scrollOffsets.top, elementOffsets[1], options, function(p)
		{
			scrollTo(scrollOffsets.left, p.round());
		}
	);
};