var renderEngine;
var interpolator;

document.observe('dom:loaded', function()
{
	renderEngine = new RenderEngine(60);
	interpolator = new Interpolator();
});

var Interpolator = Class.create(
{
	clamp: function(value, min, max)
	{
		return Math.max(min, Math.min(max, value))
	},

	linear: function(startValue, endValue, time)
	{
		return startValue + time * (endValue - startValue);
	},

	cosine: function(startValue, endValue, time)
	{
		return this.linear(startValue, endValue, -Math.cos(Math.PI * time) / 2 + 0.5);
	},

	smoothStep: function(startValue, endValue, time)
	{
		return this.linear(startValue, endValue, time * time * (3 - 2 * time));
	}
});

var RenderEngine = Class.create(
{
	initialize: function(frameRate)
	{
		this.frameDelay = (1 / frameRate) * 1000;
		this.renderables = new Array();
		this.isRunning = false;
	},

	addRenderable: function(renderable)
	{
		this.renderables.push(renderable);

		if(!this.isRunning)
		{
			this.lastTick = new Date().getTime();
			this.isRunning = true
			setTimeout(this.update.bind(this), this.frameDelay);
		};
	},

	update: function()
	{
		newTime = new Date().getTime();
		elapsed = newTime - this.lastTick;
		this.lastTick = newTime;

		for(var i = 0; i < this.renderables.length; )
		{
			if(this.renderables[i].update(elapsed))
			{
				this.renderables = this.renderables.without(this.renderables[i]);
			}
			else
			{
				++i;
			}
		}

		if(this.renderables.length > 0)
		{
			setTimeout(this.update.bind(this), this.frameDelay);
		}
		else
		{
			this.isRunning = false;
		}
	}
});


var OpacityInterpolator = Class.create(
{
	initialize: function(targetObject, targetValue, duration)
	{
		this.targetObject = targetObject;
		this.initialValue = targetObject.getOpacity();
		this.targetValue = targetValue;
		this.duration = duration * 1000;
		this.totalElapsed = 0;
	},

	update: function(elapsed)
	{
		var isDead = false;
		this.totalElapsed += elapsed;

		if(this.totalElapsed > this.duration)
		{
			this.totalElapsed = this.duration;
			isDead = true;
		}

		opacity = interpolator.linear(this.initialValue, this.targetValue, this.totalElapsed / this.duration);

		this.targetObject.setOpacity(opacity);

		return isDead;
	}
});

var SmoothRemover = Class.create(
{
	initialize: function(targetObject, duration, deleteNode)
	{
		this.targetObject = targetObject;
		this.deleteNode = deleteNode;
		this.initialHeight = targetObject.getHeight();
		this.duration = duration * 1000;
		this.empty = false;
		this.totalElapsed = 0;
	},

	update: function(elapsed)
	{
		this.totalElapsed += elapsed;

		var isDead = false;

		if(this.totalElapsed > this.duration)
		{
			this.totalElapsed = this.duration;
			isDead = true;

			if(this.deleteNode)
			{
				this.targetObject.remove();
			}
		}

		opacity = interpolator.linear(1, 0, interpolator.clamp((this.totalElapsed / this.duration) * 2, 0, 1));
		height = interpolator.smoothStep(this.initialHeight, 0, interpolator.clamp((this.totalElapsed / this.duration) * 2 - 1, 0, 1));

		this.targetObject.setOpacity(opacity);
		this.targetObject.style.height = height + 'px';

		return isDead;
	}
});

var SmoothExpander = Class.create(
{
	initialize: function(targetObject, duration)
	{
		this.originalObject = targetObject;
		this.targetObject = targetObject.wrap('div');

		this.duration = duration * 1000;
		this.empty = false;
		this.totalElapsed = 0;

		this.targetObject.setOpacity(0);
		this.targetObject.style.height = '0px';

		targetObject.setOpacity(1);
		targetObject.style.height = 'auto';
		targetObject.show();

		this.targetHeight = targetObject.getHeight();
	},

	update: function(elapsed)
	{
		this.totalElapsed += elapsed;

		var isDead = false;

		if(this.totalElapsed > this.duration)
		{
			this.targetObject.replace(this.originalObject);
			this.totalElapsed = this.duration;
			isDead = true;
		}

		// TODO: interpolate these.
		opacity = Math.min(1, ((this.totalElapsed * 2 - this.duration) / this.duration));
		height = Math.min(1, (this.totalElapsed * 2 / this.duration)) * this.targetHeight;

		this.targetObject.setOpacity(opacity);
		this.targetObject.style.height = height + 'px';

		return isDead;
	}
});

var SmoothReplacer = Class.create(
{
	initialize: function(oldObject, newObject, duration, removeOriginal)
	{
		this.totalElapsed = 0;
		this.hasSwapped = false;
		this.removeOriginal = removeOriginal;

		this.oldObject = oldObject;
		this.newObject = newObject;
		this.duration = duration * 1000;
		this.oldObjectWrapper = Element.wrap(oldObject, 'div');
		this.newObjectWrapper = Element.wrap(newObject, 'div');

		this.oldObjectWrapper.style.height = oldObject.getHeight() + 'px';

		this.newObjectWrapper.setOpacity(0);
		this.newObjectWrapper.style.height = '0px';

		newObject.setOpacity(1);
		newObject.style.height = 'auto';
		newObject.show();

		this.oldHeight = oldObject.getHeight();
		this.newHeight = newObject.getHeight();
	},

	update: function(elapsed)
	{
		var isDead = false;

		this.totalElapsed += elapsed;

		if(!this.hasSwapped && this.totalElapsed > this.duration / 2)
		{
			this.hasSwapped = true;

			temp = this.newObjectWrapper.style.height;

			this.newObjectWrapper.style.height = this.oldObjectWrapper.style.height;
			this.oldObjectWrapper.style.height = temp;
		}

		if(this.totalElapsed > this.duration)
		{
			isDead = true;
			this.totalElapsed = this.duration;

			this.oldObjectWrapper.replace(this.oldObject);
			this.newObjectWrapper.replace(this.newObject);

			if(this.removeOriginal)
			{
				this.oldObject.remove();
			}
			else
			{
				this.oldObject.hide();
			}
		}

		oldOpacity = interpolator.linear(1, 0, interpolator.clamp((this.totalElapsed / this.duration) * 2, 0, 1));
		newOpacity = interpolator.linear(0, 1, interpolator.clamp((this.totalElapsed / this.duration) * 2 - 1, 0, 1));

		this.oldObjectWrapper.setOpacity(oldOpacity);
		this.newObjectWrapper.setOpacity(newOpacity);

		if(this.newHeight > this.oldHeight)
		{
			height = interpolator.smoothStep(this.oldHeight, this.newHeight, interpolator.clamp((this.totalElapsed / this.duration) * 2, 0, 1));
		}
		else
		{
			height = interpolator.smoothStep(this.oldHeight, this.newHeight, interpolator.clamp((this.totalElapsed / this.duration) * 2 - 1, 0, 1));
		}

		if(this.hasSwapped)
		{
			this.newObjectWrapper.style.height = height + 'px';
		}
		else
		{
			this.oldObjectWrapper.style.height = height + 'px';
		}

		return isDead;
	}

});

function smoothSwapper(container, items, builder)
{
	removing = buildHash(function(item){ return item.id; }, function(item) { return item; }, container.childElements());
	staying  = $H();
	newOnes  = $H();

	items.each(function(item)
	{
		if(removing.get(item.key))
		{
			staying.set(item.key, removing.get(item.key));
			removing.unset(item.key);
		}
		else
		{
			newOnes.set(item.key, builder(item.key, item.value));
		}
	});

	swapCount = Math.min(removing.size(), newOnes.size());

	swapOut = removing.values().slice(0, swapCount);
	swapIn  = newOnes.values().slice(0, swapCount);
	remove  = removing.values().slice(swapCount);
	add     = newOnes.values().slice(swapCount);

	for(var i = 0; i < swapCount; ++i)
	{
		swapIn[i].style.height = '0px';
		swapIn[i].style.overflow = 'hidden';
		swapOut[i].insert( { after: swapIn[i] } );
		renderEngine.addRenderable(new SmoothReplacer(swapOut[i], swapIn[i], animationDuration, true));
	}

	remove.each(function(o)
	{
		container.insert( { bottom: o } );
		renderEngine.addRenderable(new SmoothRemover(o, animationDuration, true));
	});

	add.each(function(o)
	{
		o.style.height = '0px';
		container.insert( { bottom: o } );
		renderEngine.addRenderable(new SmoothExpander(o, animationDuration));
	});
}

var SmoothScroller = Class.create(
{
	initialize: function(targetObject, duration)
	{
		this.targetValue = targetObject.offsetTop;
		this.duration = duration * 1000;
		this.initialValue = document.documentElement.scrollTop;
		this.totalElapsed = 0;
	},
	
	update: function(elapsed)
	{
		var isDead = false;
		this.totalElapsed += elapsed;

		if(this.totalElapsed > this.duration)
		{
			this.totalElapsed = this.duration;
			isDead = true;
		}

		window.scrollTo(0, interpolator.smoothStep(this.initialValue, this.targetValue, this.totalElapsed / this.duration));
		
		return isDead;
	}
});
