/********************************** Browser ***********************************/
var Browser =
{
	strict:	document.compatMode == 'CSS1Compat',
	ie:		!!window.ActiveXObject,
	ie6:	!!window.ActiveXObject && !window.XMLHttpRequest,
	ie7:	!!window.ActiveXObject && !!window.XMLHttpRequest,
	gecko:	!!document.getBoxObjectFor,
	opera:	!!window.opera,
	webkit:	navigator.userAgent.indexOf('WebKit') > -1,
	khtml:	navigator.userAgent.indexOf('KHTML') > -1 && navigator.userAgent.indexOf('WebKit') == -1
};

if (Browser.ie)
{
	try { document.execCommand('BackgroundImageCache', false, true); }
	catch (e) {}
}

/*********************************** Class ************************************/
var Class =
{
	create: function(/*[className, ][parentClass, ][prototype]*/)
	{
		var arg0 = arguments[0];
		var arg1 = arguments[1];
		var arg2 = arguments[2];
		
		var className;
		var parentClass;
		var prototype;
		
		switch (typeof(arg0))
		{
			case 'string':
			
				className = arg0;
				
				if (Object.isFunction(arg1))
				{
					parentClass	= arg1;
					prototype	= arg2;
				}
				else prototype = arg1;
				
				break;
			
			case 'function':
			
				parentClass	= arg0;
				prototype	= arg1;
				break;
			
			case 'object':
				
				prototype = arg0;
				break;
		}
		
		var newClass = function()
		{
			this.constructor = newClass;
			
			if (Object.isFunction(this.initialize))
				this.initialize.apply(this, arguments);
		};
		
		if (className)
		{
			window[className]		= newClass;
			newClass.className		= className;
			newClass.instanceName	= className.firstToLowerCase();
		}
		
		newClass.parentClass	= null;
		newClass.childClasses	= [];
		newClass.toString		= this._classToString;
		
		if (parentClass)
		{
			var superClass = function() {};
			superClass.prototype = parentClass.prototype;
			
			newClass.prototype = new superClass;
			newClass.prototype.constructor = parentClass;
			newClass.parentClass = parentClass;
			parentClass.childClasses.push(newClass);
			
			newClass.extendPrototype(prototype);
		}
		else if (Object.isObject(prototype))
			newClass.prototype = prototype;
		
		newClass.prototype.toString	= this._objectToString;
		
		return newClass;
	},
	
	_classToString: function()
	{
		return '[class ' + this.className + ']';
	},
	
	_objectToString: function()
	{
		return '[object ' + this.constructor.className + ']';
	}
};

/*********************************** Object ***********************************/
Object.extend = function(dst, src, recursive)
{
	if (this.isFunctionOrObject(dst) && this.isFunctionOrObject(src))
	{
		for (var property in src)
		{
			var _dst = dst[property];
			var _src = src[property];
			
			if (recursive && this.isObject(_dst) && this.isObject(_src))
				this.extend(_dst, _src, recursive);
			else
				dst[property] = _src;
		}
	}
	
	return dst;
};

Object.isFunction = function(obj)
{
	return typeof(obj) == 'function';
};

Object.isObject = function(obj)
{
	return typeof(obj) == 'object' && obj !== null;
};

Object.isFunctionOrObject = function(obj)
{
	return this.isFunction(obj) || this.isObject(obj);
};

Object.extend(Object,
{
	isClass: function(obj)
	{
		return this.isFunction(obj) && obj.childClasses instanceof Array;
	},
	
	isNode: function(obj)
	{
		return this.isObject(obj) && obj.nodeType;
	},
	
	isElement: function(obj)
	{
		return this.isObject(obj) && obj.nodeType == 1;
	},
	
	isTextNode: function(obj)
	{
		return this.isObject(obj) && obj.nodeType == 3;
	},
	
	isBoolean: function(obj)
	{
		return typeof(obj) == 'boolean';
	},
	
	isNumber: function(obj)
	{
		return typeof(obj) == 'number';
	},
	
	isFinite: isFinite,
	isNaN: isNaN,
	
	isInteger: function(obj)
	{
		return this.isNumber(obj) && this.isFinite(obj) && obj - parseInt(obj) == 0;
	},
	
	isFloat: function(obj)
	{
		return this.isNumber(obj) && this.isFinite(obj) && obj - parseInt(obj) != 0;
	},
	
	isString: function(obj)
	{
		return typeof(obj) == 'string';
	},
	
	isNull: function(obj)
	{
		return obj === null;
	},
	
	isUndefined: function(obj)
	{
		return obj === undefined;
	},
	
	isValid: function(obj)
	{
		return obj != null && !(this.isNumber(obj) && !this.isFinite(obj));
	},
	
	defined: function(obj)
	{
		return obj !== undefined;
	},
	
	toBoolean: function(obj)
	{
		return !!obj;
	},
	
	toInteger: function(obj)
	{
		return parseInt(obj) || 0;
	},
	
	toFloat: function(obj)
	{
		return parseFloat(obj) || 0;
	},
	
	toValidString: function(obj)
	{
		return this.isValid(obj) ? '' + obj : '';
	},
	
	toQueryString: function(obj)
	{
		var r = [];
		
		for (var property in obj)
			r.push(property + '=' + obj[property]);
		
		return encodeURI(r.join('&'));
	},
	
	toArray: function(obj)
	{
		if (obj instanceof Array)
			return obj;
		
		var r = [];
		
		if (this.isFunction(obj) || this.isObject(obj))
		{
			if (this.defined(obj.length))
			{
				for (var i = 0, length = obj.length; i < length; i++)
					r[i] = obj[i];
			}
			else
			{
				for (var property in obj)
					r.push(obj[property]);
			}
		}
		
		return r;
	},
	
	getMethod: function(obj, name)
	{
		return Object.isFunctionOrObject(obj) && Object.isFunction(obj[name]) ? obj[name] : null;
	},
	
	getSetter: function(obj, name)
	{
		return this.getMethod(obj, 'set' + Object.toValidString(name).firstToUpperCase());
	},
	
	getGetter: function(obj, name)
	{
		return this.getMethod(obj, 'get' + Object.toValidString(name).firstToUpperCase());
	},
	
	callSetter: function(obj, name, value, defaultSetter)
	{
		var setter = this.getSetter(obj, name);
		
		if (setter)
			return setter.call(obj, value);
		else if (Object.isFunction(defaultSetter))
			return defaultSetter.call(obj, value);
		else
			return false;
	},
	
	callGetter: function(obj, name, defaultGetter)
	{
		var getter = this.getGetter(obj, name);
		
		if (getter)
			return getter.call(obj);
		else if (Object.isFunction(defaultGetter))
			return defaultGetter.call(obj);
		else
			return false;
	},
	
	curry: function(obj, recursive, curryValue, firstArgumentName, curryDepth)
	{
		if (!curryDepth)
			curryDepth = 0;
		
		if (curryDepth > 2)
			return obj;
		
		if (this.isObject(curryValue))
		{
			for (var property in obj)
			{
				var value = obj[property];
				
				if (recursive && this.isObject(value) && value.constructor == Object && !value.nodeType)
					this.curry(value, recursive, curryValue, firstArgumentName, ++curryDepth);
				else
				{
					if (this.isFunction(value) && value.getArgumentNames()[0] == (firstArgumentName || 'self'))
						obj[property] = value.curry(curryValue);
				}
			}
		}
		
		return obj;
	},
	
	merge: function()
	{
		var r = {};
		
		for (var i = 0, l = arguments.length; i < l; i++)
			Object.extend(r, arguments[i], true);
		
		return r;
	},
	
	clone: function(obj, recursive)
	{
		if (!this.isObject(obj)) return obj;
		
		var r = {};
		
		for (var property in obj)
		{
			if (recursive)
				r[property] = this.clone(obj[property], recursive);
			else
				r[property] = obj[property];
		}
		
		return r;
	},
	
	getTypeName: function(obj)
	{
		var typeName = typeof(obj);
		
		switch (typeName)
		{
			case 'function':	return '[' + (obj.childClasses instanceof Array ? 'class' : typeName) + ' ' + obj.getFunctionName() + ']';
			case 'object':		return obj === null ? 'null' : '[' + typeName + ' ' + obj.constructor.getFunctionName() + ']';
			default:			return typeName;
		}
	},
	
	inspect: function(obj, returnValue)
	{
		var r = '';
		
		r += 'type:\t' + this.getTypeName(obj) + '\n';
		r += 'value:\t' + obj + '\n\n';
		
		while (Object.isObject(obj))
		{
			for (var property in obj)
			{
				if (obj.hasOwnProperty(property) && property != 'constructor')
					r += property + ':\t' + obj[property] + '\n';
			}
			
			var constructor = obj.constructor;
			
			if (Object.isFunction(constructor))
				break;
			
			var prototype = constructor.prototype;
			
			if (prototype == obj)
			{
				if (prototype == Object.prototype)
					break;
				else
				{
					var constructorName = 'Object';
					prototype = Object.prototype;
				}
			}
			else if (prototype == Object.prototype)
				var constructorName = 'Object';
			else if (constructor.className)
				var constructorName = constructor.getFunctionName();
			
			r += '\n-> ' + constructorName + '.prototype\n\n';
			obj = prototype;
		}
		
		if (returnValue)
			return r;
		
		var w = window.open('about:blank');
		
		if (w)
			w.document.write('<pre>\n' + r + '</pre>');
		else
			alert(r);
	}
});

/********************************** Function **********************************/
Function.prototype.extendPrototype = function(obj)
{
	Object.extend(this.prototype, obj);
};

Function.extendPrototype(
{
	getFunctionName: function()
	{
		return this.className || Object.toValidString(this).retrieve('function', '(').trim() || '(anonymous)';
	},
	
	getArgumentNames: function()
	{
		var names = Object.toValidString(this).retrieve(/function[^\(]*\(/, ')').trim();
		return names ? names.split(/\s*,\s*/) : [];
	},
	
	bind: function()
	{
		var self	= this;
		var args	= Object.toArray(arguments);
		var obj		= args.shift();
		
		return function()
		{
			return self.apply(obj, args.concat(Object.toArray(arguments)));
		};
	},
	
	bindAsEventListener: function()
	{
		var self	= this;
		var args	= Object.toArray(arguments);
		var obj		= args.shift();
		
		return function(event)
		{
			return self.apply(obj, [event || window.event].concat(args));
		};
	},
	
	curry: function()
	{
		if (!arguments.length)
			return this;
		
		var self	= this;
		var args	= Object.toArray(arguments);
		
		return function()
		{
			return self.apply(this, args.concat(Object.toArray(arguments)));
		};
	}
});

/*********************************** Array ************************************/
Array.extendPrototype(
{
	indexOf: function(value)
	{
		for (var i = 0, l = this.length; i < l; i++)
		{
			if (this[i] === value)
				return i;
		}
		
		return -1;
	},
	
	lastIndexOf: function(value)
	{
		for (var i = this.length - 1; i > -1; i--)
		{
			if (this[i] === value)
				return i;
		}
		
		return -1;
	},
	
	contains: function(value)
	{
		return this.indexOf(value) > -1;
	},
	
	clone: function()
	{
		var r = [];
		
		for (var i = 0, l = this.length; i < l; i++)
			r.push(this[i]);
		
		return r;
	},
	
	concat: function()
	{
		var r = this.clone();
		
		for (var i = 0, il = arguments.length; i < il; i++)
		{
			if (arguments[i] instanceof Array)
			{
				for (var j = 0, jl = arguments[i].length; j < jl; j++)
					r.push(arguments[i][j]);
			}
			else r.push(arguments[i]);
		}
		
		return r;
	},
	
	each: function(iterator, context)
	{
		for (var i = 0, l = this.length; i < l; i++)
			iterator.call(context, this[i], i, this);
	},
	
	toColorText: function()
	{
		var r = '#';
		
		for (var i = 0; i < 3; i++)
		{
			var t = (Object.toInteger(this[i]) % 256).toString(16);
			r += (t.length < 2 ? '0' : '') + t;
		}
		
		return r;
	}
});

/*********************************** String ***********************************/
var STRING_RETRIEVE_APPEND_NONE		= 0;
var STRING_RETRIEVE_APPEND_START	= 1;
var STRING_RETRIEVE_APPEND_END		= 2;
var STRING_RETRIEVE_APPEND_BOTH		= 3;

String.extendPrototype(
{	
	firstToUpperCase: function()
	{
		return this.length ? this.substr(0, 1).toUpperCase() + this.substr(1) : this;
	},
	
	firstToLowerCase: function()
	{
		return this.length ? this.substr(0, 1).toLowerCase() + this.substr(1) : this;
	},
	
	camelize: function(separator)
	{
		var s = '';
		var portions = this.toLowerCase().split(separator || '-');
		
		for (var i = 0; i < portions.length; i++)
		{
			var portion = portions[i];
			s += i ? portion.firstToUpperCase() : portion;
		}
		
		return s;
	},
	
	hyphenate: function()
	{
		var s = '';
		
		for (var i = 0; i < this.length; i++)
		{
			var c = this.charAt(i);
			
			if (c >= 'A' && c <= 'Z')
				s += (i ? '-' : '') + c.toLowerCase();
			else
				s += c;
		}
		
		return s;
	},
	
	capitalize: function()
	{
		return this.replace(/\b[a-z]/g, function(match) { return match.toUpperCase(); });
	},
	
	trim: function()
	{
		return this.replace(/(^\s+)|(\s+$)/g, '');
	},
	
	stripTags: function()
	{
		return this.replace(/<[^>]*>/g, '');		
	},
	
	cssTextToObject: function()
	{
		var items	= this.split(';');
		var obj		= {};
		
		for (var i = 0; i < items.length; i++)
		{
			var pair = items[i].splitTo(':', 2);
			
			if (pair.length > 1)
			{
				var name = pair[0].trim().camelize();
				
				if (name)
					obj[name] = pair[1].trim();
			}
		}
		
		return obj;
	},
	
	styleTextToObject: function()
	{
		var matches	= this.match(/[^\{]+\{[^\}]+\}/g);
		var obj		= {};
		
		for (var i = 0; i < matches.length; i++)
		{
			var pair	= matches[i].splitTo('{', 2);
			var name	= pair[0].trim();
			
			if (name)
			{
				var value = pair[1].substr(0, pair[1].length - 1).cssTextToObject();
				obj[name] = obj[name] ? Object.extend(obj[name], value) : value;
			}
		}
		
		return obj;
	},
	
	toColorArray: function()
	{
		var s = this.startsWith('#') ? this.substr(1) : this;
		var r = [];
		
		for (var i = 0; i < 3; i++)
			r[i] = parseInt(s.substr(i * 2, 2), 16) || 0;
		
		return r;
	},
	
	setIndex: function(index)
	{
		if (this.length <= 0)
			return this.index = 0;
		
		index = Object.toInteger(index);
		
		if (index < 0)
		{
			if (index < -this.length)
				index = 0;
			else
				index += this.length;
		}
		else if (index > this.length)
			index = this.length;
		
		return this.index = index;
	},
	
	substrEx: function(startIndex, length)
	{
		startIndex = this.setIndex(startIndex);
		
		if (Object.isUndefined(length))
			return this.substr(startIndex);
		else
			return this.substr(startIndex, length);
	},
	
	indexOfEx: function(pattern, startIndex)
	{
		startIndex = Object.isValid(startIndex) ? this.setIndex(startIndex) : 0;
		
		if (pattern instanceof RegExp)
		{
			if (pattern.global)
				pattern.lastIndex = startIndex;
			else
				pattern = pattern.createByExtend({global: true, lastIndex: startIndex});
			
			var r = pattern.execEx(this);
			
			return r ? r.index : -1;
		}
		else return this.indexOf(pattern, startIndex);
	},
	
	lastIndexOfEx: function(pattern, startIndex)
	{
		startIndex = Object.isValid(startIndex) ? this.setIndex(startIndex) : this.length;
		
		if (pattern instanceof RegExp)
		{
			if (pattern.global)
				pattern.lastIndex = 0;
			else
				pattern = pattern.createByExtend({global: true, lastIndex: 0});
			
			var r;
			var t;
			
			while (t = pattern.execEx(this))
			{
				if (t.index > startIndex)
					break;
				
				r = t;
			}
			
			return r ? r.index : -1;
		}
		else return this.lastIndexOf(pattern, startIndex);
	},
	
	contains: function(pattern)
	{
		return this.indexOfEx(pattern) > -1;
	},
	
	startsWith: function(pattern)
	{
		return this.indexOfEx(pattern) === 0;
	},
	
	endsWith: function(pattern)
	{
		var lastIndex = this.lastIndexOfEx(pattern);
		
		if (lastIndex > -1)
			return lastIndex === this.length - (pattern instanceof RegExp ? RegExp.lastMatch : pattern).length;
		else
			return false;
	},
	
	retrieve: function(startPattern, endPattern, appendMode)
	{
		if (!(startPattern instanceof RegExp))
			startPattern = Object.toValidString(startPattern);
		
		if (!(endPattern instanceof RegExp))
			endPattern = Object.toValidString(endPattern);
		
		appendMode = Object.toInteger(appendMode);
		
		var startIndex;
		var startMatch;
		
		if (startPattern == '')
		{
			startIndex	= 0;
			startMatch	= startPattern;
		}
		else
		{
			startIndex = this.indexOfEx(startPattern);
			
			if (startIndex == - 1)
				return false;
			
			startMatch	= startPattern instanceof RegExp ? RegExp.lastMatch : startPattern;
			startIndex	+= startMatch.length;
		}
		
		var endIndex;
		var endMatch;
		
		if (endPattern == '')
		{
			endIndex	= this.length;
			endMatch	= endPattern;
		}
		else
		{
			endIndex = this.indexOfEx(endPattern, startIndex);
			
			if (endIndex == - 1)
				return false;
			
			endMatch = endPattern instanceof RegExp ? RegExp.lastMatch : endPattern;
		}
		
		var r = this.substring(startIndex, endIndex);
		
		if (appendMode & STRING_RETRIEVE_APPEND_START)
			r = startMatch + r;
		
		if (appendMode & STRING_RETRIEVE_APPEND_END)
			r += endMatch;
		
		return r;
	},
	
	splitTo: function(separator, limit)
	{
		var r = [];
		var startIndex = 0;
		var index;
		var i = 0;
		
		while (++i < limit && (index = this.indexOfEx(separator, startIndex)) > -1)
		{
			r.push(this.substring(startIndex, index));
			startIndex = index + (separator instanceof RegExp ? RegExp.lastMatch : separator).length;
		}
		
		r.push(this.substr(startIndex));
		return r;
	}
});

/*********************************** RegExp ***********************************/
RegExp.extendPrototype(
{
	execEx: function(string)
	{
		var r = this.exec(string);
		
		if (Browser.opera && r)
			RegExp.lastMatch = r[0];
		
		return r;
	},
	
	createByExtend: function(obj)
	{
		obj = Object.extend
		(
			{
				source:		this.source,
				global:		this.global,
				ignoreCase:	this.ignoreCase,
				multiline:	this.multiline,
				lastIndex:	this.lastIndex
			},
			obj
		);
		
		var flags = '';
		
		if (obj.global)
			flags += 'g';
		
		if (obj.ignoreCase)
			flags += 'i';
		
		if (obj.multiline)
			flags += 'm';
		
		var r = new RegExp(obj.source, flags);
		r.lastIndex = obj.lastIndex;
		
		return r;
	}
});

/************************************ Math ************************************/
Object.extend(Math,
{
	indexMod: function(value, length)
	{
		var index = Object.toInteger(value) % length;
		
		if (index < 0)
			index += length;
		
		return index;
	}
});

/*********************************** Event ************************************/
var Event =
{
	addDOMLoadListener: (function()
	{
		var listeners = [];
		var timer;
		
		var executer = function()
		{
			if (arguments.callee.called) return;
			
			arguments.callee.called = true;
			clearInterval(timer);
			
			var listener;
			
			while (listener = listeners.shift())
				listener();
		};
		
		var addExecuter = function()
		{
			if (arguments.callee.called) return;
			
			arguments.callee.called = true;
			
			if (document.addEventListener)
				document.addEventListener('DOMContentLoaded', executer, false);
			
			if (Browser.ie)
			{
				timer = setInterval(function ()
				{
					try
					{
						document.body.doScroll('left');
						executer();
					}
					catch(e) {}
				}, 10);
			}
			else if (Browser.webkit)
			{
				timer = setInterval(function()
				{
					if (/loaded|complete/.test(document.readyState))
						executer();
				}, 10);
			}
			
			var listener = function()
			{
				executer();
			};
			
			if (Browser.ie)
				window.attachEvent('onload', listener);
			else
				window.addEventListener('load', listener, false);
		};
		
		return function(listener)
		{
			if (Object.isFunction(listener))
			{
				if (executer.done)
					return listener();
				
				addExecuter();
				listeners.push(listener);
			}
		};
	})(),
	
	addEventListener: function(obj, name, listener)
	{
		if (Browser.ie)
			obj.attachEvent('on' + name, listener);
		else
			obj.addEventListener(name, listener, false);
	},
	
	removeEventListener: function(obj, name, listener)
	{
		if (Browser.ie)
			obj.detachEvent('on' + name, listener);
		else
			obj.removeEventListener(name, listener, false);
	},
	
	fireEvent: function(obj, name)
	{
		if (Browser.ie)
			obj.fireEvent('on' + name);
		else
		{
			var module;
			
			if (['abort', 'blur', 'change', 'error', 'focus', 'load', 'reset', 'resize', 'scroll', 'select', 'submit', 'unload'].contains(name))
				module = 'HTMLEvents';
			else if (['keydown', 'keyup', 'keypress', 'DOMActivate', 'DOMFocusIn', 'DOMFocusOut'].contains(name))
				module = 'UIEevents';
			else if (['click', 'dblclick', 'mouseover', 'mouseout', 'mousedown', 'mouseup', 'mousemove'].contains(name))
				module = 'MouseEvents';
			else if (['DOMAttrModified', 'DOMNodeInserted', 'DOMNodeRemoved', 'DOMCharacterDataModified', 'DOMNodeInsertedIntoDocument', 'DOMNodeRemovedFromDocument', 'DOMSubtreeModified'].contains(name))
				module = 'MutationEvents';
			else
				module = 'Events';
			
			var event = document.createEvent(module);
			
			event.initEvent(name, false, false);
			obj.dispatchEvent(event);
		}
	},
	
	getEvent: function()
	{
		if (window.event)
			return window.event;
		
		func = this.getEvent.caller;
		
		while (func != null)
		{
			var arg = func.arguments[0];
			
			if (arg && (arg.constructor == Event || arg.constructor == MouseEvent))
				return arg;
			
			func = func.caller;
		}
		
		return null;
	},
	
	getTarget: function(event)
	{
		if (!event)
			event = this.getEvent();
		
		return $(event.target || event.srcElement);
	},
	
	getPageX: function(event)
	{
		if (!event)
			event = this.getEvent();
		
		return event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft) - (document.documentElement.clientLeft || document.body.clientLeft));
	},
	
	getPageY: function(event)
	{
		if (!event)
			event = this.getEvent();
		
		return event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop) - (document.documentElement.clientTop || document.body.clientTop));
	},
	
	getPointer: function(event)
	{
		if (!event)
			event = this.getEvent();
		
		return {x: this.getPageX(event), y: this.getPageY(event)};
	},
	
	getKeyCode: function(event)
	{
		if (!event)
			event = this.getEvent();
		
		return event.keyCode;
	}
};

/********************************** Control ***********************************/
Class.create('Control',
{
	initialize: function(options)
	{
		this.options	= Object.clone(this.defaultOptions, true);
		this.listeners	= {};
		
		this.setOptions(options);
	},
	
	setOptions: function(options)
	{
		this.options = Object.extend(this.options, options, true);
		Object.curry(this.options, true, this);
		
		for (var name in this.options)
		{
			if (name.startsWith('on'))
			{
				var listenerName = name.substr(2);
				
				if (listenerName)
					this.addEventListener(listenerName.firstToLowerCase(), this.options[name]);
			}
			else this.setOption(name, this.options[name]);
		}
	},
	
	setOption: function(name, value)
	{
		this.options[name] = value;
		return Object.callSetter(this, name, value);
	},
	
	getOption: function(name)
	{
		return Object.callGetter(this, name, function ()
		{
			return this.options[name];
		});
	},
	
	addEventListener: function(name, listener)
	{
		if (Object.isString(listener))
		{
			try
			{
				eval('listener = function() { ' + listener + ' }');
			}
			catch (e)
			{
				return false;
			}
		}
		
		if (Object.isFunction(listener))
		{
			if (!(this.listeners[name] instanceof Array))
				this.listeners[name] = [];
			
			this.listeners[name].push(listener);
		}
	},
	
	removeEventListener: function(name, listener)
	{
		var listeners = this.listeners[name];
		
		if (listeners instanceof Array)
		{
			for (var i = 0, length = listeners.length; i < length; i++)
			{
				if (listeners[i] == listener)
				{
					listeners.splice(i, 1);
					break;
				}
			}
		}
	},
	
	fireEvent: function(name, arguments)
	{
		var listeners = this.listeners[name];
		
		if (listeners instanceof Array)
		{
			for (var i = 0, length = listeners.length; i < length; i++)
				listeners[i].apply(this, arguments || []);
		}
	}
});

/********************************** Element ***********************************/
var $ =	function(obj)
{
	if (Object.isObject(obj))
	{
		if (Object.isElement(obj.node))
			return obj;
		else if (obj.nodeType == 1)
		{
			if (obj.className)
			{
				classNames = obj.className.split(/\s+/);
				
				for (var i = 0, length = classNames.length; i < length; i++)
				{
					var _class = Element.classMap[classNames[i]];
					
					if (_class)
					{
						if (obj[_class.instanceName] instanceof _class)
							return obj[_class.instanceName];
						else
							return new _class({node: obj});
					}
				}
			}
			
			if (obj.element instanceof Element)
				return obj.element;
			else
				return new Element({node: obj});
		}
	}
	else if (Object.isString(obj))
		return $(document.getElementById(obj));
	else
		return null;
};

var $list = function(nodeList)
{
	var nodes = [];
	
	if (Object.isFunction(nodeList) || Object.isObject(nodeList))
	{
		var node;
		
		for (var i = 0, length = nodeList.length; i < length; i++)
		{
			if (node = $(nodeList[i]))
				nodes.push(node);
		}
	}
	
	return nodes;
};

var $name = function(name)
{
	return $list(document.getElementsByName(name));
};

var $class = function(className, element)
{
	var nodes		= [];
	var parentNode	= element ? $(element).node : document;
	var nodeList	= parentNode.getElementsByTagName('*');
	var node;
	
	for (var i = 0, length = nodeList.length; i < length; i++)
	{
		node = nodeList[i];
		
		if (node.className.split(/\s+/).contains(className))
			nodes.push($(node));
	}
	
	return nodes;
};

var $tag = function(tagName, element)
{
	var parentNode = element ? $(element).node : document;
	
	return $list(parentNode.getElementsByTagName(tagName));
};

var all = function(element)
{
	return $tag('*', element);
};

var $head =	function()
{
	return $tag('head')[0];
};

var $body =	function()
{
	return $(document.body) || $tag('body')[0];
};

Class.create('Element', Control,
{
	defaultOptions:
	{
		node: 'div'
	},
	
	customEvents: ['parsedone'],
	
	initialize: function(options)
	{
		Control.prototype.initialize.call(this, options);
		
		var listener = this.node.getAttribute('onparsedone', 0);
		
		if (listener)
			this.addEventListener('parsedone', listener);
		
		this.parseDone = true;
		this.fireEvent('parsedone');
	},
	
	setOption: function(name, value)
	{
		if (name.startsWith('transition'))
			name = 'transition';
		
		if (!this.parseDone && this.node)
			value = this.node.getAttribute(name, 0) || value;
		
		return Control.prototype.setOption.call(this, name, value);
	},
	
	isCustomEvent: function(name)
	{
		return this.customEvents.contains(name);
	},
	
	addEventListener: function(name, listener)
	{
		if (this.isCustomEvent(name))
			Control.prototype.addEventListener.call(this, name, listener);
		else
		{
			if (Object.isString(listener))
			{
				try
				{
					eval('listener = function() { ' + listener + ' }');
				}
				catch (e)
				{
					return false;
				}
			}
			
			if (Object.isFunction(listener))
				Event.addEventListener(this.node, name, listener);
		}
	},
	
	removeEventListener: function(name, listener)
	{
		if (this.isCustomEvent(name))
			Control.prototype.removeEventListener.call(this, name, listener);
		else
		{
			if (Object.isFunction(listener))
				Event.removeEventListener(this.node, name, listener);
		}
	},
	
	fireEvent: function(name, arguments)
	{
		if (this.isCustomEvent(name))
			Control.prototype.fireEvent.call(this, name, arguments);
		else
			Event.fireEvent(this.node, name);
	},
	
	setNode: function(node)
	{
		switch (typeof(node))
		{
			case 'object':
				if (node && node.nodeType == 1)
					this.node = node;
				break;
			
			case 'string':
			default:
				this.node = document.createElement(node || this.defaultOptions.node);
				break;
		}
		
		if (!this.node)
			this.node = document.createElement(this.defaultOptions.node);
		
		var instanceName = this.constructor.instanceName;
		
		this.addClass(instanceName);
		this.node[instanceName] = this;
	},
	
	appendChild: function(node)
	{
		if (node = $(node))
			this.node.appendChild(node.node);
	},
	
	setParent: function(parent)
	{
		if (parent = $(parent))
			parent.appendChild(this);
	},
	
	getParent: function()
	{
		return $(this.node.parentNode);
	},
	
	setAttribute: function(name, value)
	{
		switch (typeof(name))
		{
			case 'object':
				
				if (name)
				{
					for (var _name in name)
						this.setAttribute(_name, name[_name]);
					
					return true;
				}
				
				break;
			
			case 'string':
				
				if (name)
				{
					if (name == 'class' && Browser.ie)
						name = 'className';
					
					if (name == 'value')
						this.node.value = value;
					else
						this.node.setAttribute(name, value, 0);
					
					return true;
				}
				
				break;
		}
		
		return false;
	},
	
	getAttribute: function(name)
	{
		if (name)
		{
			if (name == 'class' && Browser.ie)
				name = 'className';
			
			if (name == 'value')
				return this.node.value;
			else
				return this.node.getAttribute(name, 0);
		}
		else return null;
	},
	
	removeAttribute: function(name)
	{
		if (name)
		{
			if (name == 'class' && Browser.ie)
				name = 'className';
			
			if (name == 'value')
			{
				delete this.node.value;
				return true;
			}
			else return this.node.removeAttribute(name, 0);
		}
		else return false;
	},
	
	parseBooleanAttribute: function(value)
	{
		return (Object.isBoolean(value) && value) || (Object.isNumber(value) && value) ||
			(Object.isString(value) && !/^(false|off|no|0)$/.test(value.toLowerCase()))
	},
	
	parseArrayAttribute: function(value)
	{
		if (Object.isString(value))
		{
			try {
				eval('value = [' + value + ']');
			}
			catch (e)
			{
				value = [];
			}
			
			return value;
		}
		else return Object.toArray(value);
	},
	
	parseObjectAttribue: function(value)
	{
		if (Object.isString(value))
		{
			try {
				eval('value = ' + (value.contains(':') ? '{' + value + '}' : value));
			}
			catch (e)
			{
				return {};
			}
		}
		
		return Object.isObject(value) ? value : {};
	},
	
	setStyle: function(name, value)
	{
		switch (typeof(name))
		{
			case 'object':
				
				if (name)
				{
					for (var _name in name)
						this.setStyle(_name, name[_name]);
					
					return true;
				}
				
				break;
			
			case 'string':
				
				if (name)
				{
					switch (name)
					{
						case 'float':
						
							name = Browser.gecko ? 'cssFloat' : 'styleFloat';
							break;
						
						case 'opacity':
						
							if (Browser.ie)
							{
								name	= 'filter';
								value	= 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + (value * 100) + ')';
							}
							
							break;
						
						case 'minWidth':
						
							if (Browser.ie6)
								name = width;
							
							break;
						
						case 'minHeight':
						
							if (Browser.ie6)
								name = height;
							
							break;
					}
					
					if (name != 'filter' && (Object.isNumber(value) || (Object.isString(value) && /^-?\d+(\.\d*)?$/.test(value)))
						&& /(width|height|left|right|top|bottom|padding|margin)$/i.test(name))
						value += 'px';
					
					this.node.style[name] = value;
					return true;
				}
				
				break;
		}
		
		return false;
	},
	
	getStyle: function(name)
	{
		if (name)
		{
			switch (name)
			{
				case 'float':
				
					name = Browser.gecko ? 'cssFloat' : 'styleFloat';
					break;
				
				case 'opacity':
				
					if (Browser.ie)
						return this.node.style.filter.alpha.opacity / 100 || 1;
					
					break;
				
				case 'minWidth':
				
					if (Browser.ie6)
						name = width;
					
					break;
				
				case 'minHeight':
				
					if (Browser.ie6)
						name = height;
					
					break;
			}
			
			var node = this.node;
			value = node.style[name] || (node.currentStyle ? node.currentStyle[name] : document.defaultView.getComputedStyle(node, null)[name]);
			
			return /px$/.test(value) ? parseInt(value) || 0 : (value == '0' ? 0 : value);
		}
		else return null;
	},
	
	setAnimation: function(value)
	{
		var obj = this.parseObjectAttribue(value);
		this.animation = obj instanceof Animation ? obj : new Animation(obj);
	},
	
	setTransition: function(value)
	{
		if (!Object.isObject(this.transition))
			this.transition = {};
		
		var obj = this.parseObjectAttribue(value);
		var animation;
		
		if (obj.animation instanceof Animation)
			animation = obj.animation;
		else if (this.animation instanceof Animation)
			animation = this.animation;
		else
			animation = this.animation = new Animation();
		
		if (!Object.isObject(obj.target))
			obj.target = this;
		
		animation.addTransition(obj);
		this.transition[obj.name || obj.property] = obj;
	},
	
	getClassList: function()
	{
		return this.node.className.split(/\s+/);
	},
	
	hasClass: function(className)
	{
		return this.getClassList().contains(className);
	},
	
	addClass: function(className)
	{
		if (!this.hasClass(className))
			this.node.className += (this.node.className ? ' ' : '') + className;
	},
	
	removeClass: function(className)
	{
		var classList	= this.getClassList();
		var index		= classList.indexOf(className);
		
		if (index > -1)
		{
			classList.splice(index, 1);
			this.node.className = classList.join(' ');
		}
	},
	
	getTagName: function()
	{
		return this.node.tagName.toLowerCase();
	},
	
	canHaveChildren: function()
	{
		return !['area', 'base', 'basefont', 'col', 'frame', 'hr', 'img', 'br', 'input', 'isindex', 'link', 'meta', 'param'].contains(this.getTagName());
	},
	
	setOuterHTML: function(value)
	{
		if (Object.defined(this.node.outerHTML))
			this.node.outerHTML = value;
		else
		{
			var r = this.node.ownerDocument.createRange();
			r.setStartBefore(this.node);
			
			var f = r.createContextualFragment(value);
			this.node.parentNode.replaceChild(f, this.node);
		}
	},
	
	getOuterHTML: function()
	{
		if (Object.defined(this.node.outerHTML))
			return this.node.outerHTML;
		
		var tagName = this.getTagName();
		var r = '<' + tagName;
		
		var a = this.node.attributes;
		
		for (var i = 0; i < a.length; i++)
		{
			if (a[i].specified)
				r += ' ' + a[i].name.toLowerCase() + '="' + a[i].value + '"';
		}
		
		return r + (this.canHaveChildren() ? '>' + this.node.innerHTML + '</' + tagName + '>' : ' />');
	},
	
	setInnerHTML: function(value)
	{
		this.node.innerHTML = value;
	},
	
	getInnerHTML: function()
	{
		return this.node.innerHTML;
	},
	
	setText: function(value)
	{
		this.node[Browser.ie ? 'innerText' : 'textContent'] = value;
	},
	
	getText: function()
	{
		return this.node[Browser.ie ? 'innerText' : 'textContent'];
	},
	
	setOpacity: function(value)
	{
		this.setStyle('opacity', value);
	},
	
	getOpacity: function()
	{
		this.getStyle('opacity');
	},
	
	makePositioned: function()
	{
		var postion = this.getStyle('position');
		
		if (!(postion == 'relative' || postion == 'absolute'))
			this.setStyle({position: 'relative'});
	},
	
	getOffset: function(target, clientOffset)
	{
		var l = 0;
		var t = 0;
		var o = this.node;
		var target = target ? $(target).node : null;
		
		do
		{
			l += o.offsetLeft || 0;
			t += o.offsetTop || 0;
			
			if (o.offsetParent == document.body && o.style.position == 'absolute')
				break;
		}
		while ((o = o.offsetParent) && (!target || o != target));
		
		if (clientOffset)
		{
			o = obj;
			
			do
			{
				if (!Browser.opera || o == document.body)
				{
					l -= o.scrollLeft || 0;
					t -= o.scrollTop || 0;
				}
			}
			while (o = o.parentNode);
		}
		
		return {left: l, top: t};
	},
	
	getOffsetWidth: function()
	{
		return this.node.offsetWidth;
	},
	
	getOffsetHeight: function()
	{
		return this.node.offsetHeight;
	},
	
	eventOccurredIn: function()
	{
		var x		= Event.getPageX();
		var y		= Event.getPageY();
		var offset	= this.getOffset();
		var left	= offset.left;
		var top		= offset.top;
		var width	= this.node.offsetWidth;
		var height	= this.node.offsetHeight;
		
		return x >= left && x < left + width && y >= top && y < top + height;
	}
});

Object.extend(Element,
{
	classMap: {element: Element},
	
	createClass: function(className, prototype)
	{
		var newClass = Class.create.apply(Class, [className, this, prototype]);
		
		if (newClass.instanceName)
			this.classMap[newClass.instanceName] = newClass;
	},
	
	parseClass: function()
	{
		var nodeList = document.getElementsByTagName('*');
		var node;
		
		for (var i = 0, iLength = nodeList.length; i < iLength; i++)
		{
			node = nodeList[i];
			
			if (node.className)
			{
				classNames = node.className.split(/\s+/);
				
				for (var j = 0, jLength = classNames.length; j < jLength; j++)
				{
					var _class = this.classMap[classNames[j]];
					
					if (_class)
						new _class({node: node});
				}
			}
		}
	}
});

Event.addDOMLoadListener(function()
{
	Element.parseClass();
});

/********************************* Animation **********************************/
Class.create('Animation', Control,
{
	defaultOptions:
	{
		delay:			0, // second
		startDelay:		0,
		rewindDelay:	0,
		duration:		1,
		frameRate:		20,
		generator:		'linear',
		descent:		false,
		autoRewind:		false
	},
	
	initialize: function(options)
	{
		Control.prototype.initialize.call(this, options);
		
		this.bindedMethods	= {start: this.start.bind(this), rewind: this.rewind.bind(this), generate: this.generate.bind(this), wait: this.wait.bind(this)};
		this.timeoutId		= null;
		this.value 			= null;
		this.delaying		= false;
		this.running		= false;
		this.paused			= false;
	},
	
	setFrameRate: function(frameRate)
	{
		this.frameRate		= frameRate;
		this.frameInterval	= 1000 / frameRate;
	},
	
	setGenerator: function(generator)
	{
		if (generator instanceof Array) // generator: ['linear', {start: 100, end: 0}]
		{
			var properties = generator[1];
			generator = generator[0];
		}
		
		if (!Object.isObject(generator))
			generator = this.constructor.generators[generator] || this.constructor.generators.linear;
		
		this.generator = Object.clone(generator);
		
		if (properties)
			Object.extend(this.generator, properties);
		
		this.generator.increment = Math.abs((this.generator.end - this.generator.start) / (this.options.duration * this.frameRate)); 
		
		if (!this.generator.precision)
			this.generator.precision = 1000;
	},
	
	setDescent: function(descent)
	{
		this.descent = descent;
	},
	
	setAutoRewind: function(autoRewind)
	{
		this.autoRewind = autoRewind;
		
		if (autoRewind)
			this.descent = !this.descent;
	},
	
	addTransition: function(obj) // obj: {(target, property[, unit]) || code, start, end}
	{
		if (Object.isObject(obj))
		{
			var listener;
			
			if (Object.isString(obj.code))
			{
				try
				{
					eval('listener = function (value) {' + obj.code.replace('{value}', obj.start + ' + ' + (obj.end - obj.start) + ' * value') + '};');
				}
				catch (e)
				{
					return false;
				}
			}
			else
			{
				listener = (function(obj, value)
				{
					if (!Object.isObject(obj.target)) return;
					
					if ((/color$/i).test(obj.property))
					{
						var start	= Object.toValidString(obj.start).toColorArray();
						var end		= Object.toValidString(obj.end).toColorArray();
						var array	= [];
						
						for (var i = 0; i < 3; i++)
							array[i] = Math.round(start[i] + (end[i] - start[i]) * value) % 256;
						
						value = array.toColorText();
					}
					else value = obj.start + (obj.end - obj.start) * value + (obj.unit || 0);
					
					if (Object.isElement(obj.target.node))
						obj.target.setStyle(obj.property, value);
					else
						obj.target[obj.property] = value;
				}).curry(obj);
			}
			
			this.addEventListener('generate', listener);
		}
	},
	
	start: function()
	{
		if (this.autoRewind)
		{
			this.rewind();
			return;
		}
		
		clearTimeout(this.timeoutId);
		
		if (this.delaying)
		{
			if (this.paused)
			{
				this.timeoutId = setTimeout(this.bindedMethods.start, 10);
				return;
			}
			else this.delaying = false;
		}
		else
		{
			var delay = this.options.startDelay > 0 ? this.options.startDelay : this.options.delay;
		
			if (delay > 0)
			{
				this.delaying	= true;
				this.timeoutId	= setTimeout(this.bindedMethods.start, delay * 1000);
				return;
			}
		}
		
		if (this.paused)
			this.paused = false;
		
		this.descent = this.options.descent;
		this.running = true;
		this.fireEvent('start');
		
		this.value = this.descent ? this.generator.end : this.generator.start;
		this.generate();
	},
	
	pause: function()
	{
		if ((this.delaying || this.running) && !this.paused)
		{
			this.paused = true;
			this.fireEvent('pause');
		}
	},
	
	resume: function()
	{
		if ((this.delaying || this.running) && this.paused)
		{
			this.paused = false;
			this.fireEvent('resume');
		}
	},
	
	rewind: function()
	{
		clearTimeout(this.timeoutId);
		
		if (this.delaying)
		{
			if (this.paused)
			{
				this.timeoutId = setTimeout(this.bindedMethods.rewind, 10);
				return;
			}
			else this.delaying = false;
		}
		else
		{
			var delay = this.options.rewindDelay > 0 ? this.options.rewindDelay : this.options.delay;
		
			if (delay > 0)
			{
				this.delaying	= true;
				this.timeoutId	= setTimeout(this.bindedMethods.rewind, delay * 1000);
				return;
			}
		}
		
		if (this.paused)
			this.paused = false;
		
		this.descent = !this.descent;
		this.fireEvent('rewind');
		
		if (!this.running)
		{
			this.running	= true;
			this.value		= this.descent ? this.generator.end : this.generator.start;			
		}
		
		this.generate();
	},
	
	stop: function()
	{
		clearTimeout(this.timeoutId);
		
		if (this.paused)
			this.paused = false;
		
		this.fireEvent('stop');
		
		this.value = this.descent ? this.generator.end : this.generator.start;
		this.fireEvent('generate', [this.generator.generate(this.value)]);
		
		this.delaying	= false;
		this.running	= false;
		
		this.fireEvent('end');
	},
	
	generate: function()
	{
		if ((!this.descent && this.value <= this.generator.end) ||
			(this.descent && this.value >= this.generator.start))
		{
			var precision		= this.generator.precision;
			var generatedValue	= this.generator.generate(this.value);
			
			if (precision)
				generatedValue = Math.round(generatedValue * precision) / precision;
			
			this.fireEvent('generate', [generatedValue]);
			
			this.value += this.generator.increment * (this.descent ? -1 : 1);
			
			if (precision)
				this.value = Math.round(this.value * precision) / precision;
			
			this.wait();
		}
		else
		{
			this.running = false;
			this.fireEvent('end');
		}
	},
	
	wait: function()
	{
		clearTimeout(this.timeoutId);
		this.timeoutId = setTimeout(this.bindedMethods[this.paused ? 'wait' : 'generate'], this.paused ? 10 : this.frameInterval);
	}
});

Animation.generators =
{
	linear:
	{
		start:	0,
		end:	1,
		
		generate: function(value)
		{
			return value;
		}
	},
	
	sin:
	{
		start:	-0.5,
		end:	0.5,
		
		generate: function(value)
		{
			return (Math.sin(value * Math.PI) + 1) / 2;
		}
	},
	
	sinacc:
	{
		start:	-0.5,
		end:	0,
		
		generate: function(value)
		{
			return Math.sin(value * Math.PI) + 1;
		}
	},
	
	sindec:
	{
		start:	0,
		end:	0.5,
		
		generate: function(value)
		{
			return Math.sin(value * Math.PI);
		}
	},
	
	expacc:
	{
		start:	-5,
		end:	0,
		base:	5,
		
		generate: function(value)
		{
			return Math.pow(this.base, value);
		}
	},
	
	expdec:
	{
		start:	0,
		end:	5,
		base:	0.2,
		
		generate: function(value)
		{
			return 1 - Math.pow(this.base, value);
		}
	},
	
	para:
	{
		start:	-1,
		end:	1,
		
		generate: function(value)
		{
			return 1 - Math.pow(value, 2);
		}			
	},
	
	paraacc:
	{
		start:	0,
		end:	1,
		
		generate: function(value)
		{
			return Math.pow(value, 2);
		}
	},
	
	paradec:
	{
		start:	-1,
		end:	0,
		
		generate: function(value)
		{
			return 1 - Math.pow(value, 2);
		}
	},
	
	paraback:
	{
		start:	0,
		end:	1,
		
		generate: function(value)
		{
			return 2 * value * (value - 0.5);
		}
	}
};
