/*
Portions based on the work of other libraries: ExtJS, YUI, JSTween
Easing equations are by Robert Penner: http://www.robertpenner.com/easing/
*/

// -- firebug 1.2 beta
if(typeof loadFirebugConsole == 'function') {
	loadFirebugConsole();
}

if(window.console === undefined) {
	var f = Prototype.emptyFunction;
	window.console = {log: f, debug: f, trace: f};
};

if(!window.Position) {
	window.Position = {};
}
Object.extend(Position, {
	// test if an object is a coordinate object
	isCoords: function(c) {
		return 'x' in c && 'y' in c && 'width' in c && 'height' in c;
	},

	// returns {x, y, width, height}
	getCoordinates: function(el) {
		el = Element.get(el);
		var d = Element.getDimensions(el), o = Element.getXY(el);
		return {x: o.left, y: o.top, width: d.width, height: d.height};
	},

	// returns how much the two element intersect each other relative to the dimension of the smaller element
	intersect: function(element, target) {
		return Position.intersectCoords(Position.getCoordinates(element), Position.getCoordinates(target));
	},

	// returns 0 to 1, the percentage of how much coord A intersect coord B based on the smaller dimensioned coords
	intersectCoords: function(a, b) {
		var ar = a.x + a.width, ab = a.y + a.height;
		var br = b.x + b.width, bb = b.y + b.height;
		var hx = Math.min(Math.max(Math.min((ar - b.x), (br - a.x)), 0), a.width);
		var vx = Math.min(Math.max(Math.min((ab - b.y), (bb - a.y)), 0), a.height);
		var aa = a.width * a.height;
		var ba = b.width * b.height;
		return Math.min(((hx * vx) / Math.min(aa, ba)), 1);
	},

	// returns true if element lies completely inside the parent
	// similar to intersect == 1
	inside: function(el, parent) {
		return Position.insideCoords(Position.getCoordinates(el), Position.getCoordinates(parent));
	},

	// returns true if coordinates A lie inside coordinates B
	insideCoords: function(a, b) {
		return a.x >= b.x && a.y >= b.y && a.width <= b.width && a.height <= b.height;
	},

	// test if (x, y) is within coord
	withinCoordinates: function(coord, x, y) {
		return (x >= coord.x && x <= (coord.x + coord.width))
		 && (y >= coord.y && y <= (coord.y + coord.height));
	},

	getRegions: function(el) {
		var c = Position.getCoordinates(el);
		return {top: c.y, right: c.x+c.width, bottom: c.y+c.height, left: c.x};
	},

	withinRegion: function(r, x, y) {
		return x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
	}
});


/* Cookie Hash ----------------------------------------------------------------------------------------------- */
/* from: http://gorondowtl.sourceforge.net/wiki/Cookie */
/* ----------------------------------------------------------------------------------------------------------------*/
var Cookie = {
	get: function(name) {
		var start = document.cookie.indexOf(name + "=");
		var len = start + name.length + 1;
		if (!start && (name != document.cookie.substring(0, name.length)))
			return null;
		if(start == -1)
			return null;
		var end = document.cookie.indexOf( ";", len);
		if(end == -1)
			end = document.cookie.length;
		return unescape(document.cookie.substring(len, end));
	},

	// expires is in days
	set: function(name, value, expires, path, domain, secure) {
		var today = new Date();
		today.setTime(today.getTime());
		if(expires)
			expires = expires * 1000 * 60 * 60 * 24;
		var expires_date = new Date(today.getTime() + (expires));
		document.cookie = name + "=" + escape(value)
		+ ((expires) ? ";expires="+expires_date.toGMTString() : "")
		+ ((path) ? ";path=" + path : "" )
		+ ((domain) ? ";domain=" + domain : "")
		+ ((secure) ? ";secure" : "");
	},

	erase: function(name, path, domain) {
		if(Cookies.get(name))
			document.cookie = name + "=" + ((path) ? ";path=" + path : "") +
		((domain) ? ";domain=" + domain : "" ) + ";expires=Thu, 01-Jan-1970 00:00:01 GMT";
	},

	accept: function() {
		if(typeof navigator.cookieEnabled == 'boolean')
			return navigator.cookieEnabled;
		Cookie.set('_test', '1');
		return (Cookie.erase('_test') === '1');
	}
};

var StateMgr = (function() {
	function getStateId(id) {
		return 'x_prototype_state_data_' + id;
	}
	return {
		set: function(id, stateData) {
			if(!id) return;
			Cookie.set(getStateId(id), Object.toJSON(stateData), 30, '/');
		},

		get: function(id) {
			var str = Cookie.get(getStateId(id));
			if(!Object.isString(str)) return undefined;
			var stateData = str.evalJSON();
			return (typeof stateData != 'object') ? undefined: stateData;
		}
	};
})();


// From  Ext.util.MixedCollection (www.extjs.com) with minor changes and using Enumerable
/* -------------------------------------------------------------------------  */
var Collection = Class.create(Enumerable, Event.Observable.prototype, {
	initialize: function(getKeyFn) {
		Event.Observable.prototype.initialize.call(this);
		this.items = [];
		this.map = {}; // map[key] => item
		this.keys = [];
		this.length = 0;
		if(Object.isFunction(getKeyFn)) {
			this.getKey = getKeyFn;
		}
	},

	add: function(k, o) {
		if(arguments.length == 1) {
			o = arguments[0];
			k = this.getKey(o);
		}
		var vk = k !== undefined && k !== null;
		if(vk && this.map[k])
			return this.replace(k, o);
		this.length++;
		this.items.push(o);
		this.keys.push(vk ? k : null);
		if(vk) this.map[k] = o;
		this.fire('Add', this, o, this.length-1);
		return o;
	},

	addAll: function(objs) {
		if(arguments.length > 1 || Object.isArray(objs)) {
			var args = arguments.length > 1 ? arguments : objs;
			for(var i = 0, l = args.length; i < l; ++i)
				this.add(args[i]);
		} else {
			for(var key in objs)
				this.add(key, objs[key]);
		}
	},

	insert: function(i, k, o) {
		if(arguments.length == 2) {
			o = arguments[1];
			k = this.getKey(o);
		}
		if(i >= this.length)
			return this.add(k, o);
		this.items.splice(i, 0, o);
		this.keys.splice(i, 0, k);
		if(k !== undefined && k !== null)
			this.map[k] = o;
		this.length++;
		this.fire('Add', this, o, i);
		return o;
	},

	item: function(k) {
		return this.map[k] !== undefined ? this.map[k] : this.items[k];
	},

	itemAt: function(i) {
		return this.items[i];
	},

	remove: function(o) {
		return this.removeAt(this.keys.indexOf(o));
	},

	removeKey: function(k) {
		return this.removeAt(this.indexOfKey(k));
	},

	removeAt: function(i) {
		if(i >= 0 && i < this.length) {
			this.length--;
			var r = this.items[i];
			this.items.splice(i, 1);
			this.keys.splice(i, 1);
			if(k !== undefined)
				delete this.map[this.keys[i]];
			this.fire('Remove', this, r);
			return r;
		}
		return false;
	},

	replace: function(k, o) {
		if(arguments.length == 1) {
			o = arguments[0];
			k = this.getKey(o);
		}
		var r = this.item(k);
		if(k !== undefined || k === null || r === undefined)
			return this.add(k, o);
		var i = this.indexOfKey(k);
		this.items[i] = o;
		this.map[k] = o;
		this.fire('Replace', this, o, k, r);
		return o;
	},

	size: function() {
		return this.length;
	},

	getKey: function(o) {
		return o.id;
	},

	indexOfKey: function(k) {
		return this.keys.indexOf(k);
	},

	indexOf: function(o) {
		return this.items.indexOf(o);
	},

	key: function(k) {
		return this.map[k];
	},

	include: function(o) {
		return this.indexOf(o) != -1;
	},

	clear: function() {
		this.length = 0;
		delete this.items; this.items = [];
		delete this.keys; this.keys = [];
		delete this.map; this.map = {};
		this.fire('Clear', this);
	},

	first: function() {
		return this.items[0];
	},

	last: function() {
		return this.items[this.length-1];
	},

	sort: function(order, fn) {
		this._sort('value', order, fn);
	},

	keySort: function(order, fn) {
		this._sort('key', order, fn || function(a, b) {
			return String(a).toUpperCase() - String(b).toUpperCase();
		});
	},

	getRange: function(start, end) {
		var items = this.items, r = [];
		if(items.length >= 1) {
			start = start || 0;
			end = Math.min(end === undefined ? this.length - 1 : end, this.length - 1);
			if(start <= end) for(var i = start; i <= end; ++i) r[r.length] = items[i];
			else for(var i = start; i >= end; --i) r[r.length] = items[i];
		}
		return r;
	},

	/* -- privates */

	_sort: function(property, order, fn) {
		var dsc = String(order).toLowerCase() == 'desc' ? -1 : 1;
		fn = fn || function(a, b) {return a - b};

		var c = [], k = this.keys, items = this.items;
		for(var i = 0, l = items.length; i < l; ++i)
			c[c.length] = {key: k[i], value: items[i], index: i};

		c.sort(function(a, b) {
			var v = fn(a[property], b[property]) * dsc;
			if(v == 0) v = (a.index < b.index ? -1 : 1);
			return v;
		});

		for(var i = 0, len = c.length; i < len; i++) {
			items[i] = c[i].value;
			k[i] = c[i].key;
		}
		this.fire("Change", this, 'sort');
	},

	// for Enumerable
	_each: function(iterator) {
		for(var i = 0, l = this.length; i < l; ++i) {
			iterator.call(this.items[i], this.items[i], i, this.keys[i]);
		}
	}
});

/* aliases */
(function() {
	var p = Collection.prototype;
	p.addAt = p.insert;
	p.get = p.item;
	p.getAt = p.itemAt;
})();


/* Data Store & Readers, based on ExtJS ----------------------------------------*/

var Data = {
	RECORD_ID_COUNTER: 1
};

Data.SortTypes = {
	none: function(s) {
		return s;
	},

	stripTagsRE: /<\/?[^>]+>/gi,

	asText: function(s){
		return String(s).replace(this.stripTagsRE, "");
	},

	asUCText: function(s){
		return String(s).toUpperCase().replace(this.stripTagsRE, "");
	},

	asUCString: function(s) {
		return String(s).toUpperCase();
	},

	asFloat: function(s) {
		var val = parseFloat(String(s).replace(/,/g, ""));
		if(isNaN(val)) val = 0;
		return val;
	},

	asInt: function(s) {
		var val = parseInt(String(s).replace(/,/g, ""));
		if(isNaN(val)) val = 0;
		return val;
	},

	unitRe: /([KMGT])B?\s*$/i,

	asSizeText: function(s) {
		s = String(s).replace(/[\,\s]+/g, '');
		var v = Data.SortTypes.asFloat(s);
		var m = s.match(Data.SortTypes.unitRe);
		if(m) {
			var u = String(m[1]).toUpperCase();
			switch(u) {
				case 'T': v *= 1024;
				case 'G': v *= 1024;
				case 'M': v *= 1024;
				case 'K': v *= 1024;
			}
		}
		return v;
	}
};

Data.Field = Class.create({
	id: '',
	defaultValue: '',
	mapping: null,
	sortType: null,
	sortDir: 'ASC',
	type: null,

	initialize: function(options) {
		if(typeof options == 'string')
			options = {id: options};
		Object.extend(this, options);

		var st = Data.SortTypes;
		if(Object.isString(this.sortType))
			this.sortType = st[this.sortType];
		if(!this.sortType) {
			switch(this.type) {
				case 'string': this.sortType = st.asUCString; break;
				case 'float': this.sortType = st.asFloat; break;
				case 'int': this.sortType = st.asInt; break;
				default: this.sortType = st.none;
			}
		}
		if(!this.convert) {
			var cv, stripRe = /[\$,%]/g;
			switch(this.type) {
				case "string":
					cv = function(v) {return (v === undefined || v === null) ? '' : String(v);};
				break;
				case "int":
					cv = function(v) {
						return v !== undefined && v !== null && v !== '' ?
							parseInt(String(v).replace(stripRe, ""), 10) : '';
					};
				break;
				case "float":
					cv = function(v) {
						return v !== undefined && v !== null && v !== '' ?
							parseFloat(String(v).replace(stripRe, ""), 10) : '';
					};
				break;
				case "bool":
				case "boolean":
					cv = function(v){ return v === true || v === "true" || v == 1; };
				break;
				default: cv = function(v) { return v };
			}
			this.convert = cv;
		}
	}
});

/*
Base reader class.

fields: array of fields in a record. Each entry is an object containing the field's options. Field options include:
	id: (required, string), the field name/id
	mapping: (optional, string/integer), used by the different reader implementations to get the data for this field
	type: specify the data type of this field for when sorting.
		Possible values are: string, int, float.
		If undefined, sorting is done using primative comparison.
	sort: a sort function that takes in two records and returns -1, 0, or 1.
totalRecords: mixed, determines where the total records value can be found. See implementations.
recordId: mixed value, the mapping to get the record id.
	This is the same as having a field called "id" and providing a mapping for it. See implementations.

Example:
var reader = new Data.Reader({
	recordId: 'something...',
	fields: [
		{id: 'username', type: 'string'},
		{id: 'password', mapping: mixed}
	]
});

reader.read() returns a record objects (hash), each record contains "username" and "password" property
*/



Data.Reader = Class.create(Class.baseMethods, {
	initialize: function(options) {
		this.fields = new Collection();
		options = options || {};
		var fields = options.fields;
		if(fields && Object.isArray(fields)) {
			var st = Data.SortTypes;
			for(var i = 0, l = fields.length; i < l; ++i) {
				this.fields.add(new Data.Field(fields[i]));
			}
			delete options.fields;
		}
		this.setOptions(options);
	}
});

/*
Read an array of arrays.
totalRecords: ignored, total records is always the length of the returned records
recordId: index where the record ID can be found
mapping: map the field to a certain index in the entry. Defaults to the index of the field.
	Ex: [
		{id: 'name'}, 				// value is at index 0
		{id: 'email', mapping: 4}, 	// value is at index 4
		{id: 'password'} 			// value is at index 2
	]
*/
Data.ArrayReader = Class.create(Data.Reader, {
	readRecords: function(data) {
		var records = [], rid = this.options.recordId;
		if(Object.isArray(data)) {
			for(var i = 0, l = data.length; i < l; ++i) {
				var d = data[i], rec = {};
				rec.id = ((rid || rid === 0) && d[rid] !== undefined && d[rid] !== "" ? d[rid] : null);
				this.fields.each(function(f, j) {
					if(f.mapping !== undefined && f.mapping !== null) {
						j = f.mapping;
					}
					rec[f.id] = f.convert(d[j]);
				});
				records[records.length] = rec;
			}
		}
		return {
			'records': records,
			'totalRecords': records.length
		}
	}
});

/*
 Read an array of objects from a JSON object.
   root, totalRecords, recordId: json expression to the property in the JSON object that holds the data to be read.
   Ex: 'items' is JSON.items, 'items.newItems' is JSON.items.newItems.
 mapping: map the field to a certain property in the entry object. Defaults to the field id
	Ex: [
		{id: 'name'}, 								// value at property obj.name
		{id: 'email', mapping: 'email_address'} 	// value is at property obj.email_address
	]
*/
Data.JSONReader = Class.create(Data.Reader, {
	initialize: function(options) {
		this.jsonData = null;

		Data.JSONReader.parentClass.initialize.call(this, (Object.extend({
			root: '',
			totalRecords: ''
		}, options || {})));
	},

	// read responseJSON or evaluated responseText
	read: function(transport) {
		if(transport.responseJSON) {
			return this.read(transport.responseJSON);
		} else {
			var json;
			try { json = transport.responseText.evalJSON(); }
			catch(e) { throw {message: 'Data.JSONReader.read: Response text is not valid JSON'}; }
			return this.readRecords(json);
		}
	},

	/* -- privates */

	makeAccessor: function(expr) {
		return (String(expr).indexOf('.') >= 0)
		  ? new Function("obj", "return obj." + expr)
		  : function (obj) { return obj[expr] };
	},

	readRecords: function(json) {
		this.jsonData = json
		var records = [], totalRecords = 0, o = this.options;

		if(!this.fieldAccessors) {
			this.getTotalRecords = o.totalRecords ? this.makeAccessor(o.totalRecords) : null;
			this.getRoot = o.root ? this.makeAccessor(o.root) : function(obj) {return obj};
			this.getRecordId = o.recordId
				? this.makeAccessor(o.recordId).wrap(function(p, r) {
					var id = p(r);
					return (id === undefined || id === '') ? null : id;
				})
				: function() {return null};

			this.fieldAccessors = [];
			this.fields.each(function(f, i) {
				this.fieldAccessors[i] = this.makeAccessor(f.mapping || f.id);
			}, this);
		}

		var root = this.getRoot(json), fa = this.fieldAccessors;
		if(Object.isArray(root)) {
			totalRecords = root.length;
			if(this.getTotalRecords) {
				totalRecords = this.getTotalRecords(json);
			}
			for(var i = 0, l = root.length; i < l; ++i) {
				var d = root[i], rec = {};
				rec.id = this.getRecordId(d);
				this.fields.each(function(f, j) {
					rec[f.id] = f.convert(fa[j](d));
				}, this);
				records[records.length] = rec;
			}
		}
		return {
			'records': records,
			'totalRecords': totalRecords || records.length
		};
	}
});

/*
 Read an XML doc.
   root, totalRecords, recordId: css selector to select the node where the value can be located
 mapping: map the field to a certain node tagName/attribute. Defaults to the node with the tagName that is the same as the field ID.
	Ex: [
		{id: 'name'}, 								// value for this field is inside node with tagName "name". Ex: <name>bob</name>
		{id: 'email', mapping: 'email_address'}, 	// <email_address>bob@somewhere.com</email_address>
		{id: 'age', mapping: '@age'}				// value is at attribute "age". Ex: <item age="24">...</item>
	]
*/
Data.XMLReader = Class.create(Data.Reader, {
	initialize: function(options) {
		Data.XMLReader.parentClass.initialize.call(this, (Object.extend({
			root: '',
			record: '',
			totalRecords: ''
		}, options || {})));

		this.xmlData = null;
	},

	read: function(transport) {
		var doc = transport.responseXML;
		if(!doc) {
			throw {message: 'Data.XMLReader.read: XML Document not available'};
		}
		return this.readRecords(doc);
	},

	/* -- privates */

	readRecords: function(doc) {
		this.xmlData = doc;
		var records = [], totalRecords = 0, root = doc.documentElement || doc;
		if(this.options.root && root.tagName != this.options.root) {
			var r = Selector.select(root, this.options.root);
			root = r ? r[0] : r;
		}
		if(this.options.totalRecords) {
			totalRecords = Selector.selectNumber(root, this.options.totalRecords, 0);
		}
		var recs = Selector.select(root, this.options.record);
		for(var i = 0, l = recs.length; i < l; ++i) {
			var recNode = recs[i];
			var rec = {};
			this.fields.each(function(f, i) {
				var v = Selector.selectValue(recNode, f.mapping || f.id, f.defaultValue || '');
				v = f.convert(v);
				rec[f.id] = v;
			}, this);
			records[records.length] = rec;
		}
		return {
			'records': records,
			'totalRecords': totalRecords || records.length
		};
	}
});

Data.Store = Class.create(Enumerable, Event.Observable.prototype, Class.baseMethods, {
	initialize: function(options) {
		Event.Observable.prototype.initialize.call(this);
		options = options || {};

		this.data = new Collection;
		this.data.getKey = function(o) {
			return !Object.isEmpty(o.id) ? o.id : (o.id = Data.RECORD_ID_COUNTER++);
		};

		if(options.reader) {
			this.reader = options.reader;
			delete options.reader;
		} else {
			throw 'No reader specified in Data.Store.';
		}

		this.setOptions({
			sortField: '',
			sortOrder: '',

			// for remote sorting
			url: null,
			method: 'post',
			remoteSort: false,
			baseParams: {},
			preventCache: true,
			paramNames: {
				sort: 'sort',
				order: 'order',
				start: 'start',
				limit: 'limit'
			}
		}, options);
	},

	add: function(records) {
		records = [].concat(records);
		if(records.length < 1) return;
		var index = this.data.length;
		this.data.addAll(records);
		this.fire('Add', this, records, index);
	},

	remove: function(record) {
		var index = this.data.indexOf(record);
		this.data.remoteAt(index);
		this.fire('Remove', this, record, index);
	},

	removeAll: function() {
		this.data.clear();
		this.fire('Clear', this);
	},

	insert: function(index, records) {
		records = [].concat(records);
		// repeated inserts at the same index results in reverse order of records,
		// so the need to reverse the loop here
		for(var i = records.length-1; i >= 0; --i) {
			this.data.insert(index, records[i]);
		}
		this.fire('Add', this, records, index);
	},

	indexOf: function(record) {
		return this.data.indexOf(record);
	},

	indexOfId: function(id) {
		return this.data.indexOfKey(id);
	},

	getById: function(id) {
		return this.data.key(id);
	},

	getAt: function(index) {
		return this.data.getAt(index);
	},

	getRange: function(start, end) {
		return this.data.getRange(start, end);
	},

	getCount: function() {
		return this.data.length;
	},

	getTotalCount: function() {
		return this.totalLength || 0;
	},

	getReader: function() {
		return this.reader;
	},

	// options will also be passed to loadRecords
	load: function(options) {
		options = Object.extend({
			url: this.options.url,
			method: this.options.method,
			preventCache: this.options.preventCache,
			parameters: {},
			callback: undefined,
			scope: undefined
		}, options || {});

		this.storeOptions(options);

		if(this.fire('BeforeLoad', this, options) !== false) {
			var p = Object.extend(options.parameters || {}, this.options.baseParams || {});

			var o = this.options;
			if(o.sortField && o.remoteSort) {
				var pn = o.paramNames;
				p[pn['sort']] = o.sortField;
				p[pn['order']] = o.sortOrder;
			}
			if(options.preventCache) {
				p['__random'] = new Date().getTime();
			}
			options.parameters = p;

			var self = this;
			this.ajax = new Ajax.Request(options.url, {
				parameters: p,
				method: options.method,
				/* use timeout because these are called inside try catch block which hides all exceptions, not cool */
				onSuccess: function(transport) {
					setTimeout(function() {
						self.onSuccess(options, transport);
					}, 10);
				},
				onFailure: function(transport) {
					setTimeout(function() {
						self.onFailure(options, transport);
					}, 10);
				}
			});
			return true;
		}
		return false;
	},

	loadData: function(data, append) {
		if(!this.reader || !data) return;
		this.loadRecords(this.reader.readRecords(data), {'append': append});
	},

	getSortInfo: function() {
		return {
			field: this.options.sortField,
			order: this.options.sortOrder
		};
	},

	setSortInfo: function(field, order) {
		this.options.sortField = field;
		this.options.sortOrder = order;
	},

	sort: function(f, order) {
		var o = this.options,
			st = this.sortToggle || (this.sortToggle = {});
		order = String(order).toLowerCase();
		f = f || o.sortField;
		if(!f) return;

		if(order != 'asc' && order != 'desc') {
			order = (st[f] == 'asc' || (o.sortField == f && o.sortOrder == 'asc') ? 'desc' : 'asc');
		}
		o.sortField = f;
		o.sortOrder = st[f] = order;

		if(o.remoteSort) {
			this.load(Object.extend({append: false}, this.lastOptions || {}));
		} else {
			var st = this.reader.fields.get(f).sortType;
			var fn = function(a, b) {
				var x = st(a[f]), y = st(b[f]);
				return x < y ? -1 : (x == y ? 0 : 1);
			};
			this.data.sort(order, fn);
			this.fire('Change', this, 'sort');
		}
	},

	/* -- privates */

	storeOptions: function(o) {
		o = Object.clone(o);
		delete o.callback;
		delete o.scope;
		this.lastOptions = o;
	},

	loadRecords: function(o, options) {
		options = Object.extend({
			append: false,
			callback: false,
			scope: false
		}, options || {});

		var r = o.records, t = o.totalRecords || r.length;
		if(!options.append) {
			this.data.clear();
			this.data.addAll(r);
			this.totalLength = t;
			this.applySort();
			this.fire('Change', this, 'append');
		} else {
			this.totalLength = Math.max(t, this.data.length + r.length);
			this.add(r);
		}
		this.fire('Load', this, r, options);

		if(options.callback) {
			options.callback.call(options.scope || this, r, options);
		}
	},

	applySort: function() {
		var o = this.options;
		if(o.sortField && !o.remoteSort) {
			this.sort(o.sortField, o.sortOrder);
		}
	},

	// callback for Ajax.Request
	onSuccess: function(options, transport) {
		this.loadRecords(this.reader.read(transport), options);
	},

	onFailure: function(options, transport) {
		this.fire('LoadFailure', this, transport, options);
	},

	// for Enumerable
	_each: function(iterator) {
		this.data.each(function(record) {
			iterator(record);
		}, this);
	}
});

Data.SimpleStore = Class.create(Data.Store, {
	initialize: function(options) {
		options = options || {};
		var fields = {};
		if(options.fields) {
			fields = options.fields;
			delete options.fields;
		}
		options.reader = new Data.ArrayReader({fields: fields});
		Data.SimpleStore.parentClass.initialize.call(this, options);
	}
});


var DragDrop = {};


// Better Drag Drop implementation, based on YUI
DragDrop.Mgr = {
	activeDrag:		null,
	pointer: 		null,
	initialPointer: null,

	useOverlay: 	false,
	sensitivity:	0,
	clickThresh:	800,

	isDragging:		false,
	isOverlaying: 	false,

	setOverlayUsage: function(use) {
		this.useOverlay = use;
	},

	getPointerDelta: function() {
		var p = this.pointer, ip = this.initialPointer;
		return (!p || !ip) ? {x: 0, y: o} : {x: p.x - ip.x, y: p.y - ip.y};
	},

	addDDToGroup: function(g, dd) {
		var dds = this.groups[g] || (this.groups[g] = {});
		dds[dd.id] = dd;
	},

	removeDDFromGroup: function(g, dd) {
		var dds = this.groups[g];
		if(dds) {
			delete dds[dd.id];
		}
	},

	/* -- privates */
	currentTarget: null,
	isDocEventAttached: false,
	clickThreshTimer: null,
	groups:	{},

	onHdlMouseDown: function(drag, event) {
		this.currentTarget = event.element();
		this.activeDrag = drag;

		this.pointer = event.pointer();
		this.initialPointer = event.pointer();
		this.sensitivity = drag.options.sensitivity || 0;

		this.isDragging = false;
		this.attachToDoc();

		if(this.clickThreshTimer) {
			clearTimeout(this.clickThreshTimer);
			this.clickThreshTimer = null;
		}
		this.clickThreshTimer = setTimeout(function() {
			var p = DragDrop.Mgr.initialPointer;
			DragDrop.Mgr.startDrag(p.x, p.y);
		}, this.clickThresh);
	},

	onMouseMove: function(event) {
		var p = this.pointer = event.pointer();
		var drag = this.activeDrag;

		if(!this.isDragging) {
			var ip = this.initialPointer;
			var dx = p.x - ip.x;
			var dy = p.y - ip.y;
			var s = this.sensitivity;

			if(!s || (Math.abs(dx) >= s || Math.abs(dy) >= s)) {
				this.startDrag(ip.x, ip.y);
			}
		}

		if(this.isDragging) {
			if(false !== drag.b4Drag(event) !== false
			 && false !== drag.fire('Drag', drag, event)) {
				drag.onDrag(event);
				this.processDD('drag', event);
			}
		}
	},

	onMouseUp: function(event) {
		this.stopDrag(event);
		this.activeDrag.onMouseUp(event);
	},

	onKeyPress: function(event) {
		if(event.keyCode == Event.KEY_ESC) {
			this.activeDrag.isCancelled = true;
			this.stopDrag(event);
		}
	},

	startDrag: function(x, y) {
		clearTimeout(this.clickThreshTimer);
		this.clickThreshTimer = null;
		var drag = this.activeDrag;

		if(x === undefined || y === undefined) {
			x = this.initialPointer.x;
			y = this.initialPointer.y;
		}
		if(false !== drag.fire('BeforeDragStart', drag, x, y)
		 && false !== drag.b4StartDrag(x, y)
		 && false !== drag.fire('DragStart', drag, x, y)
		 && false !== drag.startDrag(x, y)) {
			this.isDragging = true;
			if(this.useOverlay || drag.options.useOverlay === true) {
				this.showOverlay();
			}
			this.processDD('start');
		}
	},

	stopDrag: function(event) {
		clearTimeout(this.clickThreshTimer);
		this.clickThreshTimer = null;
		var drag = this.activeDrag;
		if(this.isDragging) {
			drag.b4EndDrag(event);
			drag.endDrag(event);
			drag.fire('DragEnd', drag, event);
		}
		this.processDD('end', event);
		this.isDragging = false;
		this.hideOverlay();
		this.detachFromDoc();
	},

	showOverlay: function() {
		if(!this.isOverlaying) {
			this.isOverlaying = true;
			var mask = $(document.body).mask(false, false, 'ui-drag-overlay');
			var dragEl = this.activeDrag.getDragEl();
			if(dragEl) {
				mask.style.zIndex = (parseInt(dragEl.getStyle('zIndex'), 10) || 1) - 1;
			}
		}
	},

	hideOverlay: function() {
		if(this.isOverlaying) {
			this.isOverlaying = false;
			document.body.unmask();
		}
	},

	processDD: function(dragEvent, event) {
		var drag = this.activeDrag;
		if(!drag || drag.options.dragOnly || !this.isDragging) {
			return;
		}

		var ip = this.initialPointer, x = ip.x, y = ip.y;
		var targets = [];

		for(var g in drag.groups) {
			for(var id in this.groups[g]) {
				var t = this.groups[g][id];
				if(t.options.isTarget && !t.disabled && t != drag) {
					targets[targets.length] = t;
				}
			}
		}

		for(var i = 0, l = targets.length; i < l; ++i) {
			var t = targets[i];

			if(dragEvent == 'start') {
				t.isDragOver = false;
				if(false !== t.fire('DragStart', t, x, y)) {
					t.startDrag(x, y);
				}
			} else if(dragEvent == 'end') {
				if(!drag.isCancelled && t.isDragOver) {
					if(false !== drag.fire('Drop', drag, t, event)) {
						drag.onDrop(t, event);
					}
				}
				t.fire('DragEnd', t, event);
				t.endDrag(event);
			} else if(dragEvent == 'drag') {
				var over = this.isOverTarget(t.options.tolerance, t, drag);

				if(over) {
					var m = 'DragOver', e = 'on'+m;
					if(false !== drag.fire(m, drag, t, event)) {
						drag[e](t, event)
					}
				}
				if((over && !t.isDragOver) || (!over && t.isDragOver)) {
					var m = over ? 'DragEnter' : 'DragOut', e = 'on'+m;
					t.isDragOver = over;
					if(false !== drag.fire(m, drag, t, event)) {
						drag[e](t, event)
					}
				}
			}
		}
	},

	isOverTarget: function(tolerance, target, drag) {
		var dc = drag.getCoordinates();
		var tc = target.getCoordinates();
		var p = this.pointer;

		switch(tolerance) {
			case 'near': case 'pointer-near':
				var d = target.options.distance;
				d = Object.isNumber(d) ? {x: d, y: d} : d;
				var tcx = {
					x: tc.x - d.x,
					y: tc.y - d.y,
					width: tc.width + (d.x * 2),
					height: tc.height + (d.y * 2)
				};
				return tolerance == 'near'
					? Position.intersectCoords(d, tcx) > 0
					: Position.withinCoordinates(tcx, p.x, p.y);
			case 'inside': case 'intersect':
				var i = Position.intersectCoords(dc, tc);
				return tolerance == 'inside' ? i == 1 : i > 0;
			case 'pointer': default:
				return Position.withinCoordinates(tc, p.x, p.y);
			break;
		}
	},

	attachToDoc: function() {
		if(!this.isDocEventAttached) {
			this.isDocEventAttached = true;
			document.on({
				mousemove: this.onMouseMove,
				mouseup:   this.onMouseUp,
				keypress:  this.onKeyPress,
				selectstart: Event.stop,
				drag: Event.stop
			}, this);
		}
	},

	detachFromDoc: function() {
		if(this.isDocEventAttached) {
			this.isDocEventAttached = false;
			document.off({
				mousemove: this.onMouseMove,
				mouseup:   this.onMouseUp,
				keypress:  this.onKeyPress,
				selectstart: Event.stop,
				drag: Event.stop
			}, this);
		}
	}
};

DragDrop.DDBase = Class.create(Event.Observable, Class.baseMethods, {
	deltaX: 0,
	deltaY: 0,

	initialX: 0,
	initialY: 0,

	currentX: 0,
	currentY: 0,

	isDragOver: false,
	isCancelled: false,

	initialize: function(el, options) {
		DragDrop.DDBase.parentClass.initialize.call(this);

		this.handle = null;
		this.dragEl = null;
		this.element = null;
		this.id = null;
		this.data = null;
		this.groups = {};

		var o = options || {};

		this.setEl(el);
		this.setHandleEl(o.handle || el);
		this.setDragEl(o.dragEl || el);

		delete o.handle;
		delete o.dragEl;

		this.data = o.data;
		this.addToGroup(o.group || 'default');
		delete o.data;
		delete o.group;

		this.setOptions({
			// drag distance threshold
			sensitivity: 0,

			// revert on drag cancel
			revert: true,

			// target related options
			isTarget: true,

			// pointer|inside|intersect|near|pointer-near
			tolerance: 'pointer',

			// for tolerance == near|pointer-near
			distance: {x: 0, y: 0}
		}, o);
	},

	start: function(event) {
		this.onHdlMouseDown(event);
	},

	stop: function() {
		DragDrop.Mgr.stopDrag();
	},

	destroy: function() {
		this.setHandleEl();
		this.off();
	},

	setHandleEl: function(el) {
		if(this.handle) {
			this.handle.off('mousedown', this.onHdlMouseDown, this);
			this.handle.off('drag', Event.stop);
			delete this.handle;
		}
		if((this.handle = $(el))) {
			this.handle.on('mousedown', this.onHdlMouseDown, this);
			this.handle.on('drag', Event.stop);
		}
	},

	getDragEl: function() {
		return this.dragEl;
	},

	setDragEl: function(el) {
		this.dragEl = $(el);
	},

	getEl: function() {
		return this.element;
	},

	setEl: function(el) {
		this.element = $(el);
		this.id = Element.identify(el);
	},

	// get XY of dragEl
	getXY: function() {
		return {x: this.currentX, y: this.currentY};
	},

	// coordinates of dragEl
	getCoordinates: function() {
		return Object.extend(
			this.getXY(),
			this.elDimCache || (this.elDimCache = this.getDragEl().getDimensions())
		);
	},

	addToGroup: function(g) {
		var gs = Object.isArray(g) ? g : arguments;
		for(var i = 0, l = gs.length; i < l; ++i) {
			this.groups[gs[i]] = true;
			DragDrop.Mgr.addDDToGroup(gs[i], this);
		}
	},

	removeFromGroup: function(g) {
		var gs = Object.isArray(g) ? g : arguments;
		for(var i = 0, l = gs.length; i < l; ++i) {
			if(this.groups[gs[i]]) {
				delete this.groups[gs[i]];
				DragDrop.Mgr.removeDDFromGroup(gs[i], this);
			}
		}
	},

	setInitialXY: function(x, y) {
		if(x === undefined || y === undefined) {
			var xy = this.element.getXY();
			x = xy.x;
			y = xy.y;
		}
		this.initialX = x;
		this.initialY = y;

		this.currentX = x;
		this.currentY = y;
	},

	/* -- privates */

	onHdlMouseDown: function(event) {
		if(event.isLeftClick()) {
			if(false !== this.onMouseDown(event)) {
				event.stop();
				this.isCancelled = false;
				DragDrop.Mgr.onHdlMouseDown(this, event);
			}
		}
	},

	// in order
	onMouseDown: function(event) {},
	b4StartDrag: function(x, y) {},
	startDrag: function(x, y) {},

	b4Drag: function(event) {},
	onDrag: function(event) {},

	onDragEnter: function(target, event) {},
	onDragOver: function(target, event) {},
	onDragOut: function(target, event) {},
	onDrop: function(target, event) {},

	b4EndDrag: function(event) {},
	endDrag: function(event) {},
	onMouseUp: function(event) {}
});

DragDrop.DD = Class.create(DragDrop.DDBase, {
	initialize: function(el, options) {
		var o = Object.extend({
			moveX: true,
			moveY: true,

			minX: null,
			maxX: null,
			minY: null,
			maxY: null,

			useOverlay: false,
			dragOnly: false
		}, options || {});

		DragDrop.DD.parentClass.initialize.call(this, el, o);
	},

	setDragElPos: function(x, y) {
		this.alignElWithMouse(this.getDragEl(), x, y);
	},

	setXConstraints: function(min, max) {
		this.options.minX = min;
		this.options.maxX = max;
		this.hasXContraints = true;
	},

	setYConstraints: function(min, max) {
		this.options.minY = min;
		this.options.maxY = max;
		this.hasYContraints = true;
	},

	getDragDelta: function() {
		var xy = this.getXY();
		return {
			x: xy.x - this.initialX,
			y: xy.y - this.initialY
		};
	},

	setDelta: function(x, y) {
		this.deltaX = x;
		this.deltaY = y;
	},

	autoOffset: function(x, y) {
		this.setDelta(x - this.initialX, y - this.initialY);
	},

	alignElWithMouse: function(el, x, y) {
		var f = this.filterXY(x, y);
		x = f.x;
		y = f.y;

		this.currentX = x;
		this.currentY = y;

		if(!this.deltaSetXY) {
			el.setXY(x, y);
			this.deltaSetXY = [
				(parseInt(el.style.left, 10) || 0) - x,
				(parseInt(el.style.top, 10) || 0) - y
			];
		} else {
			el.style.left = this.deltaSetXY[0] + x + 'px';
			el.style.top = this.deltaSetXY[1] + y + 'px';
		}
	},

	/* -- privates */

	filterXY: function(x, y) {
		var o = this.options;

		x -= this.deltaX;
		y -= this.deltaY;

		if(o.moveX === false) x = this.initialX;
		if(o.moveY === false) y = this.initialY;

		if(this.hasXContraints) {
			var a = o.minX, b = o.maxX;
			if(Object.isNumber(a) && x < a) {
				x = a;
			} else if(Object.isNumber(b) && x > b) {
				x = b;
			}
		}
		if(this.hasYContraints) {
			var a = o.minY, b = o.maxY;
			if(Object.isNumber(a) && y < a) {
				y = a;
			} else if(Object.isNumber(b) && y > b) {
				y = b;
			}
		}
		return {x: x, y:  y};
	},

	b4StartDrag: function(x, y) {
		this.setInitialXY();
		this.autoOffset(x, y);
	},

	startDrag: function(x, y) {
		delete this.elDimCache;
		delete this.deltaSetXY;

		var o = this.options;
		var el = this.getDragEl();

		// make element movable
		var pos = el.getStyle('position');
		if(!pos || pos == 'static') {
			el.relativize();
		}

		var p = DragDrop.Mgr.pointer;
		this.setDragElPos(p.x, p.y);
	},

	b4Drag: function(event) {
		var p = DragDrop.Mgr.pointer;
		this.setDragElPos(p.x, p.y);
	},

	endDrag: function(event) {
		if(this.isCancelled) {
			this.setDelta(0, 0);
			this.setDragElPos(this.initialX, this.initialY);
		}
	}
});

// Drag an element using a proxy element (frame)
DragDrop.DDProxy = Class.create(DragDrop.DD, {
	initialize: function(el, options) {
		var o = Object.extend({
			// resize frame to the element
			resizeFrame: true,

			// position frame to the element's XY position
			positionFrame: true,

			// center frame around cursor
			centerFrame: false,

			// move element to frame at the end of the drag
			moveToFrame: true,

			// usually we don't drop on proxies
			isTarget: false
		}, options || {});

		DragDrop.DDProxy.parentClass.initialize.call(this, el, o);
	},

	/* -- privates */

	b4StartDrag: function(x, y) {
		var el = this.getEl(), dragEl = this.getDragEl(), o = this.options;

		// create frame
		if(!dragEl || dragEl === el) {
			if(!(dragEl = DragDrop.Mgr.proxyEl)) {
				dragEl = DragDrop.Mgr.proxyEl = $(document.body).createChild('div', {
					id: 'dd-proxy',
					style: {
						border: '1px solid red',
						position: 'absolute',
						visibility: 'hidden',
						cursor: 'move',
						zIndex: 999
					}
				});
			}
			this.setDragEl(dragEl);
		}
		dragEl.show('block').makeInvisible();

		// resize
		if(o.resizeFrame) {
			dragEl.setDimensions(el.getDimensions());
		}
		// position and show
		if(o.positionFrame) {
			dragEl.setXY(el.getXY());
		}

		dragEl.makeVisible();
		DragDrop.DDProxy.parentClass.b4StartDrag.call(this, x, y);

		// center frame
		if(o.centerFrame) {
			var dd = dragEl.getDimensions();
			this.setDelta(Math.floor(dd.width / 2), Math.floor(dd.height / 2));
		}
	},

	b4EndDrag: function(event) {
		var el = this.getEl(), dragEl = this.getDragEl();
		if(!this.isCancelled && this.options.moveToFrame) {
			el.setXY(dragEl.getXY());
		}
		dragEl.makeInvisible();
		DragDrop.DDProxy.parentClass.b4EndDrag.call(this, event);
	}
});

// "Drag" an element using a special proxy. The element is never moved, only the proxy.
DragDrop.ByProxy = Class.create(DragDrop.DDProxy, {
	initialize: function(el, options) {
		var o = options || {};
		if(o.proxy) {
			this.proxy = o.proxy;
			delete o.proxy;
		} else {
			this.proxy = new DragDrop.StatusProxy();
		}
		Drag.ByProxy.parentClass.initialize.call(this, el, o);
		this.options.moveToFrame = false;
	},

	autoOffset: function(x, y) {
		var po = this.proxy.options.pointerOffset;
		this.setDelta(po.x, po.y);
	},

	alignElWithMouse: function(el, x, y) {
		Drag.ByProxy.parentClass.alignElWithMouse.call(this, el, x, y);
		this.proxy.sync();
	},

	/* -- privates */

	b4StartDrag: function(x,  y) {
		this.proxy.init(this, x, y);
		Drag.ByProxy.parentClass.b4StartDrag.call(this, x, y);
	},

	startDrag: function(x, y) {
		this.proxy.show();
		Drag.ByProxy.parentClass.startDrag.call(this, x, y);
		this.onStartDrag(x, y);
	},

	onStartDrag: function(x, y) {
		var el = this.getEl();
		var clone = el.cloneNode(true);
		clone.id = 'clone-of-' + Element.identify(el);
		this.proxy.setContent(clone);
	},

	b4EndDrag: function(event) {
		Drag.ByProxy.parentClass.b4EndDrag.call(this, event);
		this.proxy.hide();
	},

	onDragOver: function(target, event) {
		this.proxy.setStatus(target.onDragOver(this, event));
	},

	onDragOut: function(target, event) {
		this.proxy.setStatus(target.onDragOut(this, event));
	}
});

DragDrop.StatusProxy = Class.create(Class.baseMethods, {
	initialize: function(options) {
		this.accepted = true;
		this.setOptions({
			pointerOffset: {x: -12, y: -20},
			className: '',
			acceptCls: 'ui-drag-proxy-accepted',
			rejectCls: 'ui-drag-proxy-rejected'
		}, options || {});
	},

	setContent: function(content) {
		this.contentEl.update(content);
		this.layer.sync();
	},

	setStatus: function(accepted) {
		if(accepted !== this.accepted) {
			var o = this.options;
			this.element.replaceClassName(
				o[this.accepted ? 'acceptCls' : 'rejectCls'],
				o[accepted ? 'acceptCls' : 'rejectCls']
			);
			this.accepted = accepted;
		}
	},

	show: function() {
		this.layer.show();
	},

	hide: function() {
		this.layer.hide();
	},

	/* -- privates */

	sync: function() {
		this.layer.sync();
	},

	init: function(drag, x, y) {
		if(!this.element) {
			this.layer = new UI.Layer({className: 'ui-drag-proxy ' + this.options.className});
			this.element = this.layer.element;
			this.element.update('<div class="ui-drag-proxy-icon"></div><div class="ui-drag-proxy-content">&nbsp;</div>');
			this.contentEl = this.element.down('.ui-drag-proxy-content');
			drag.setDragEl(this.element);
			drag.setOptions({resizeFrame: false});
		}
		this.accepted = true;
		this.setStatus(false);
	}
});

var Drag = {
	Move: DragDrop.DD,
	Proxy: DragDrop.DDProxy,
	ByProxy: DragDrop.ByProxy,
	Mgr: DragDrop.Mgr
};

var Drop = {};

Drop.Target = Class.create(DragDrop.DDBase, {
	initialize: function(el, options) {
		var o = Object.extend({
			activeCls: 	'dragging',
			overCls: 	'over',
			acceptCls: 	'accept',
			rejectCls: 	'reject'
		}, options || {});
		o.isTarget = true;
		Drop.Target.parentClass.initialize.call(this, el, o);
	},

	isValidDrop: function(drag) {
		return true;
	},

	/* -- privates */
	setHandleEl: Prototype.emptyFunction,
	startDrag: Prototype.emptyFunction,

	startDrag: function() {
		this.getEl().addClassName(this.options.activeCls);
		this.setInitialXY();
	},

	endDrag: function() {
		this.getEl().removeClassName(this.options.activeCls);
		this.applyCls();
	},

	onDragEnter: function(drag, event) {
		var v = this.isValidDrop(drag);
		this.applyCls(true, v);
		return v;
	},

	onDragOut: function(drag, event) {
		var v = this.isValidDrop(drag);
		this.applyCls(false, v);
		return v;
	},

	applyCls: function(over, accepted) {
		var o = this.options, el = this.getEl();
		if(over) {
			el.addClassName(o.overCls);
			el.addClassName(accepted ? o.acceptCls :o.rejectCls);
		} else {
			el.removeClassName(o.overCls);
			el.removeClassName(o.acceptCls);
			el.removeClassName(o.rejectCls);
		}
	}
});

// logic from Ext.dd.DropZone
Drop.Zone = Class.create(Drop.Target, {
	initialize: function(el, options) {
		Drop.Zone.parentClass.initialize.call(this, el, options);
		this.lastOverNode = null;
	},

	/* -- private */

	getTargetFromEvent: function(event) {
		return event.element();
	},

	onDragOver: function(drag, event) {
		var n = this.getTargetFromEvent(event);
		if(!n) {
			if(this.lastOverNode) {
				this.onNodeOut(this.lastOverNode, drag, event);
				this.lastOverNode = null;
			}
			return this.onContainerOver(drag, event);
		}
		if(this.lastOverNode != n) {
			if(this.lastOverNode) {
				this.onNodeOut(this.lastOverNode, drag, event);
			}
			this.onNodeEnter(n, drag, event);
			this.lastOverNode = n;
		}
		return this.onNodeOver(n, drag, event);
	},

	onDragOut: function(drag, event) {
		if(this.lastOverNode) {
			this.onNodeOut(this.lastOverNode, drag, event);
			this.lastOverNode = null;
		}
	},

	onDrop: function(drag, event) {
		if(this.lastOverNode){
            this.onNodeOut(this.lastOverNode, drag, event);
            this.lastOverNode = null;
        }
        var n = this.getTargetFromEvent(event);
        return n ? this.onNodeDrop(n, drag, event)
				 : this.onContainerDrop(drag, event);
	},

	onContainerOver: function(drag, event) {
	},

	onContainerDrop: function(drag, event) {
	},

	onNodeOver: function(node, drag, event) {
	},

	onNodeOut: function(node, drag, event) {
	},

	onNodeEnter: function(node, drag, event) {
	},

	onNodeDrop: function(node, drag, event) {
	}
});

/* Controls ------------------------------------------------------------------------------------------------------ */
/* ----------------------------------------------------------------------------------------------------------------*/
var Controls = {};


Controls.SelectionModel = Class.create(Event.Observable.prototype, Class.baseMethods, {
	initialize: function(options) {
		Event.Observable.prototype.initialize.call(this);
		this.setOptions({
			// only one selection can be made (like radio inputs)
			singleSelect: false,
			// don't clear existing selection when a selection is made (like checkboxes)
			// this only works if singleSelect = false
			checkSelect: false
		}, options);
		this.selections = [];
		this.last = false;
	},

	getCount: function() {
		return this.selections.length;
	},

	getSelections: function() {
		return [].concat(this.selections);
	},

	isSelected: function(index) {
		return this.selections.include(this.selectionItem(index));
	},

	clearSelections: function(fast) {
		if(fast !== true) {
			var s = [].concat(this.selections);
			for(var i = 0, l = s.length; i < l; ++i) {
				this.deselect(this.selectionIndex(s[i]));
			}
		}
		this.selections = [];
		this.last = false;
	},

	deselect: function(index) {
		if(this.last == index) {
			this.last = false;
		}
		var r = this.selectionItem(index);
		if(r) {
			this.selections.remove(r);
			this.onDeselect(index);
			this.fire('Deselect', index);
		}
	},

	select: function(index, keepExisting) {
		if(!this.checkIndex(index)) {
			return;
		}
		if(!this.options.checkSelect && (!keepExisting || this.options.singleSelect))
			this.clearSelections();
		this.last = index;
		var r = this.selectionItem(index);
		if(r) {
			this.selections[this.selections.length] = r;
			this.onSelect(index);
			this.fire('Select', index);
		}
	},

	selectFirst: function() {
		this.select(0);
	},

	selectLast: function() {
		this.select(this.selectionCount()-1);
	},

	selectAll: function() {
		for(var i = 0, l = this.selectionCount(); i < l; ++i) {
			this.select(i, true);
		}
	},

	selectRange: function(start, end, keepExisting) {
		if(!keepExisting)
			this.clearSelections();
		if(start <= end) {
			for(var i = start; i <= end; ++i)
				this.select(i, true);
		} else {
			for(var i = start; i >= end; --i)
				this.select(i, true);
		}
	},

	/* -- privates */

	// returns index of a selected item
	// this MUST be overridden
	selectionIndex: function(item) {
		return item;
	},

	// returns the item at the selection index
	// this MUST be overridden
	selectionItem: function(index) {
		return index;
	},

	// returns the total number of selectable items
	// this MUST be overridden
	selectionCount: function() {
		throw {message: 'Extend me'};
	},

	checkIndex: function(index) {
		return index >= 0 && index < this.selectionCount();
	},

	// this should be called by the child class
	onClick: function(index, event) {
		if(event.shiftKey && this.last !== false) {
			var last = this.last;
			this.selectRange(last, index, event.ctrlKey);
			this.last = last;
		} else {
			var isSelected = this.isSelected(index);
			if((event.ctrlKey || this.options.checkSelect) && isSelected) {
				this.deselect(index);
			}else if(!isSelected || this.getCount() > 1){
				this.select(index, event.ctrlKey || event.shiftKey);
			}
		}
	},

	onSelect: function(index) {
	},

	onDeselect: function(index) {
	}
});


Controls.Forms = {};

/* allows multiple select with shift+click selection */
Controls.Forms.CheckboxSelection = Class.create(Controls.SelectionModel, {
	initialize: function(form, options) {
		if(!(this.form = $(form)))
			return;
		options = Object.extend({
			name: ''
		}, options || {});

		Controls.Forms.CheckboxSelection.parentClass.initialize.call(this, options);
		this.form.on('click', this.onFormClick.bindEvent(this));
		this.populate();
		this.eventDisabled = false;
	},

	// populate checkboxes in the form
	// call when checkboxes are added/removed from the form
	populate: function() {
		this.checkboxes = [];
		this.checkboxes = this.form.getInputs('checkbox', this.options.name);
	},

	/* -- privates */

	selectionIndex: function(cb) {
		return this.checkboxes.indexOf(cb);
	},

	selectionItem: function(i) {
		return this.checkboxes[i];
	},

	selectionCount: function(i) {
		return this.checkboxes.length;
	},

	onFormClick: function(event) {
		if(this.eventDisabled)
			return;
		var el = event.element(), index = -1;
		if(!el || el.tagName != 'INPUT' || el.type != 'checkbox' || (this.options.name && el.name != this.options.name))
			return;
		if((index = this.checkboxes.indexOf(el)) < 0)
			return;
		this.eventDisabled = true;
		this.onClick(index, event);
		this.eventDisabled = false;
	},

	onSelect: function(index) {
		var cb = this.checkboxes[index];
		if(cb && !cb.checked) {
			cb.leftClick();
		}
	},

	onDeselect: function(index) {
		var cb = this.checkboxes[index];
		if(cb && cb.checked) {
			cb.leftClick();
		}
	}
});


// Requires Menu
Controls.Forms.AutoComplete = Class.create(Event.Observable, Class.baseMethods, {
	initialize: function(element, options) {
		Controls.Forms.AutoComplete.parentClass.initialize.call(this);
		this.menu = new UI.Menu.ListMenu(null, {
			className: 'ui-menu ui-auto-complete-menu'
		});
		this.element = $(element);
		this.value = this.element.value;
		this.results = [];

		this.setOptions({
			acs: null,
			requestDelay: 500 // delay after a keystroke before sending the request
		}, options);

		// copy over
		this.acs = this.options.acs;
		this.acs.on('Result', this.handleResult.bind(this));

		this.obs = {
			keyup: this.keyup.bindEvent(this),
			keydown: this.keydown.bindEvent(this),
			request: this.request.bind(this)
		}
		this.element.setAttribute('autocomplete', 'off');
		this.element.on('keyup', this.obs.keyup);
		this.element.on('keydown', this.obs.keydown);

		this.menu.on('ItemClick', this.menuClick.bind(this));
		this.cache = {};
	},

	/* -- privates */

	/* events */
	keyup: function(event) {
		if(this.element.value != this.value) {
			this.value = this.element.value;
			if(this.requestTimer) {
				clearTimeout(this.requestTimer);
				this.requestTimer = null;
			}
			this.requestTimer = setTimeout(this.obs.request, this.options.requestDelay);
		}
	},

	keydown: function(event) {
		switch(event.keyCode) {
			case Event.KEY_DOWN:
				if(!this.menu.visible && this.results.length) {
					this.menu.show();
				}
			break;
		}
	},

	menuClick: function(item) {
		if(item._acResultIndex === undefined)
			return;
		var r = this.results[item._acResultIndex];
		var v = Object.isString(r) ? r : r.value;
		this.element.value = v;
		this.value = v;
	},

	// make a request to the URL
	request: function() {
		this.fire('Request', this);
		this.acs.request(this.value);
	},

	handleResult: function(result) {
		this.results = result;
		this.displayResult();
	},

	displayResult: function() {
		this.menu.removeAllItems();
		if(this.results.length) {
			this.results.each(function(r, i) {
				var l = Object.isString(r) ? r : r.label;
				var v = Object.isString(r) ? r : r.value;
				var item = this.menu.add({label: l});
				item._acResultIndex = i;
			}, this);
			var ew = this.cache.elementWidth || (this.cache.elementWidth = this.element.getWidth());
			this.menu.setStyle({width: ew + 'px'});
			this.menu.show(this.element, 'tl-bl');
		} else if(this.menu.visible) {
			this.menu.hide();
		}
	}
});

/* base class for Auto Complete service */
Controls.Forms.ACSBase = Class.create(Event.Observable, {
	initialize: function() {
		Controls.Forms.ACSBase.parentClass.initialize.call(this);
		this.results = [];
	},

	/* privates */
	handleResult: function() {
		this.fire('Result', this, this.results);
	}
});

/*
  Local Auto Complete service, you supply the data in an array.
  var acsl = new Controls.Forms.ACSLocal(['one', 'two', 'three']);
  To be used in combo inputs.
*/
Controls.Forms.ACSLocal = Class.create(Controls.Forms.ACSBase, {
	initialize: function(data) {
		Controls.Forms.ACSLocal.parentClass.initialize.call(this);
		this.data = data || [];
	},

	request: function(req) {
		// reset the results
		this.results = [];
		if(req) {
			// go through the data and match
			var rx = new RegExp('^('+RegExp.escape(req)+')', 'img');
			for(var i = 0; i < this.data.length; ++i) {
				var d = this.data[i];
				if(d == req) {
					this.results.push(d);
				} else if(rx.match(d)) {
					this.results.push({label: d.replace(rx, '<strong>$1</strong>', 'i'), value: d});
				}
			}
		}
		this.handleResult();
	}
});

/*
  Remote auto complete service. Options are the as those in Ajax Options
  with the addition of these below:
  url: url to make the request
  varName: the variable name of the request to be added in the params option.

  You may specify Ajax Options also. For example:
  var acrs = new Controls.Forms.ACSRemote({
	url: 'www.site.com',
	varName: 'name',

	// Ajax Options options
	parameters: {....},
	method: 'get'
  });

  The response from the server should be an array in JSON format. If you want to use another
  format, overwrite the processResponse method of the object.

  acrs.processResponse = function(transport) {
	// your own implementation here
	// remember to set this.results and call handleResult
  }
*/
Controls.Forms.ACSRemote = Class.create(Controls.Forms.ACSBase, Class.baseMethods, {
	initialize: function(options) {
		Controls.Forms.ACSRemote.parentClass.initialize.call(this, options);
		this.setOptions({
			url: '',
			varName: ''
		}, options);
		this.ajax = null;
	},

	request: function(req) {
		this.results = [];
		if(this.ajax)
			delete this.ajax;
		var op = Object.clone(this.options);
		op.onSuccess = this.processResponse.bind(this);
		if(!op.parameters) op.parameters = {};
		op.parameters[op.varName] = req;

		this.ajax = new Ajax.Request(this.options.url, op);
	},

	// this function should always set this.results and then call this.handleResult()
	processResponse: function(transport) {
		var r = transport.responseJSON;
		if(Object.isArray(r)) {
			this.results = r;
			this.handleResult();
		}
	}
});

/*
 Basic tab control class. Each tab element must have a class name that is in a
 certain format and contains the ID of the container to show/hide.

 <ul id="my-tab">
   <li class="tab-one">One</li>
   <li class="tab-two">Two</li>
   <li>Clicking on me will not show any tab</li>
 </ul>
 <div id="pages">
   <div id="one">One</div>
   <div id="two">Two</div>
   <div>I will always be hidden</div>
 </div>

 new Controls.BasicTab('my-tab', 'pages');

*/
Controls.BasicTab = Class.create(Event.Observable, {
	// the pattern to extract the id of the container from the className of the
	// tab element
	classRegExp: /tab\-([\S]+)/i,
	// selector for the element (relative to the tabs container)
	tabSelector: 'li',
	// selector for the pages (relative to pages container)
	pageSelector: 'div',
	// class name to add to the active class element
	activeTabCls: 'active',

	initialize: function(tabsContainer, pagesContainer, options) {
		this.tabsEl = $(tabsContainer);
		this.pagesEl = $(pagesContainer);
		Object.extend(this, options || {});
		this.init();
	},

	init: function() {
		this.activeTab = '';
		this.reload();
		this.tabsEl.on('click', this.onClick, this);
	},

	show: function(id) {
		if(id != this.activeTab) {
			this.pages.each(function(p) {
				if(p.id == id) {
					if(false !== this.fire('Show', this, id, p)) {
						p.show();
					}
				} else {
					if(false !== this.fire('Hide', this, this.activeTab, p)) {
						p.hide();
					}
				}
			}, this);
			this.tabs.each(function(t) {
				t[t.hasClassName('tab-'+id) ? 'addClassName' : 'removeClassName'](this.activeTabCls);
			}, this);
			this.activeTab = id;
		}
	},

	// can be called again to reload cached children
	reload: function() {
		this.tabs = this.tabsEl.select(this.tabSelector);
		this.pages = this.pagesEl.select(this.pageSelector);
	},

	/* -- privates */

	onClick: function(event) {
		event.stop();
		var el = event.findElement(this.tabSelector);
		if(el && this.classRegExp.test(el.className || '')) {
			this.show(RegExp.$1);
		}
	}
});

/*---------------------------- UI STUFF --------------------------------------*/

var UI = {};

(function() {
	UI.Element = Class.create(Event.Observable, {
		initialize: function(el) {
			UI.Element.parentClass.initialize.call(this);
			this.element = Element.get(el);
		}
	});

	var p = UI.Element.prototype;
	var methods = ['makeInvisible', 'makeVisible', 'show', 'hide', 'visible',
	  'setXY', 'setX', 'setY', 'getXY', 'getX', 'getY',
	  'setLeftTop', 'setLeft', 'setTop', 'getLeftTop', 'getLeft', 'getTop',
	  'setDimensions', 'setWidth', 'setHeight',
	  'getDimensions', 'getWidth', 'getHeight',
	  'alignTo'
	];

	methods.each(function(m) {
		var em = m;
		if(m == 'visible') {
			m = 'isVisible';
		}
		p[m] = function() {
			var ret = Element.Methods[em].apply(this, [this.element].concat($A(arguments)));
			return ret === this.element ? this : ret;
		}
	});
})();

UI.ComponentMgr = (function() {
	var comps = new Collection();
	return {
		register: function(c) {
			comps.add(c);
		},

		unregister: function(c) {
			comps.remove(c);
		},

		get: function(id) {
			return comps.get(id);
		}
	};
}());

UI.getCmp = function(id) {
	return UI.ComponentMgr.get(id);
}

UI.Component = Class.create(UI.Element, Class.baseMethods, {
	rendered: false,
	disabled: false,
	hidden: false,
	hideMode: 'display',
	hideParent: false,
	boxReady: false,

	initialize: function(options) {
		Event.Observable.prototype.initialize.call(this);
		if(!options) options = {};

		Object.extend2(this, options,
			['id', 'hidden', 'width', 'height', 'disabled', 'hidden',
			  'autoWidth', 'autoHeight', 'hideParent'],
		true);

		this.setOptions({
			saveState: true
		}, options);

		this.getId();
		this.initComponent();

		if(this.options.saveState) {
			this.initState();
		}

		if(this.options.renderTo) {
			this.container = $(this.options.renderTo);
			delete this.options.renderTo;
			this.render(this.container);
		} else if(this.options.element) {
			this.element = $(this.options.element);
			delete this.options.element;
			this.render(this.element.parentNode);
		}
		UI.ComponentMgr.register(this);
	},

	render: function(container, position) {
		if(!this.rendered) {
			var opt = this.options;
			this.rendered = true;
			this.container = (!container && this.element ? $(this.element.parentNode) : $(container));
			this.onRender(this.container, (Object.isNumber(position) ? $(container.childNodes[position]) : $(position)) || null);

			if(opt.className) {
				this.element.addClassName(opt.className);
				delete opt.className;
			}
			if(opt.style) {
				this.element.setStyle(opt.style);
				delete opt.style;
			}
			this.fire('Render', this);
			this.afterRender(this.container);

			if(this.hidden) this.hide();
			if(this.disabled) this.disable();
		}
	},

	getId: function() {
		return this.id || Prototype.getId(this, 'ui-comp-');
	},

	getEl: function() {
		return this.element;
	},

	isVisible: function() {
		return this.rendered && this.getActionEl().visible();
	},

	setDisabled: function(disabled) {
		this[disabled ? 'disable' : 'enable']();
	},

	disable: function() {
		if(this.rendered) {
			this.onDisable();
		}
		this.disabled = true;
		this.fire('Disable', this);
	},

	enable: function() {
		if(this.rendered) {
			this.onEnable();
		}
		this.disabled = false;
		this.fire('Enable', this);
	},

	show: function() {
		if(this.fire('BeforeShow', this) !== false) {
			this.hidden = false;
			if(this.rendered)
				this.onShow();
			this.fire('Show', this);
		}
	},

	hide: function() {
		if(this.fire('BeforeHide', this) !== false) {
			this.hidden = false;
			if(this.rendered)
				this.onHide();
			this.fire('Hide', this);
		}
	},

	setLeftTop: function(x, y) {
		if(x && typeof x == 'object') {
			y = x.y; x = x.x;
		}
		this.left = x;
		this.top = y;

		if(!this.boxReady){
			return this;
		}
		var el = this.getPositionEl();
		if(x !== undefined || y !== undefined) {
			el.setLeftTop(x, y);
			this.onSetLeftTop(x, y);
			this.fire('Move', this, x, y);
		}
		return this;
	},

	setXY: function(x, y) {
		if(x && typeof x == 'object') {
			y = x.y; x = x.x;
		}
		this.x = x;
		this.y = y;
		if(!this.boxReady || x === undefined || y === undefined) {
			return this;
		}
		var el = this.getPositionEl();
		var pts = el.translatePoints(x, y);
		this.setLeftTop(pts.left, pts.top);
		return this;
	},

	getXY: function() {
		return this.xy || this.element.getXY();
	},

	setDimensions: function(w, h) {
		if(w && typeof w == 'object') {
			h = w.height; w = w.width;
		}
		if(!this.boxReady) {
			this.width = w;
			this.height = w;
			return this;
		}
		if(this.lastDim && this.lastDim.width == w && this.lastDim.height == h) {
			return this;
		}
		this.lastDim = {width: w, height: h};

		var adj = this.adjustDimensions(w, h);
		var aw = adj.width, ah = adj.height;

		if(aw !== undefined || ah !== undefined) {
			var rz = this.getResizeEl();
			if(aw !== undefined && ah !== undefined) {
				rz.setDimensions(aw, ah);
			} else if(ah !== undefined) {
				rz.setHeight(ah);
			} else if(aw !== undefined) {
				rz.setWidth(aw);
			}
			this.onResize(w, h);
			this.fire('Resize', this, aw, ah, w, h);
		}
		return this;
	},

	setSize: function() {
		this.setDimensions.apply(this, arguments);
	},

	getSize: function() {
		return this.getDimensions();
	},

	refreshDimensions: function() {
		delete this.lastDim;
		this.setDimensions(this.element.getDimensions());
		return this;
	},

	addClassName: function(cls) {
		if(this.element) {
			this.element.addClassName(cls);
		} else {
			this.className = (this.className || '') + ' ' + cls;
		}
	},

	removeClassName: function(cls) {
		if(this.element) {
			this.element.removeClassName(cls);
		} else if(this.className) {
			this.className = this.className.split(' ').without(cls).join(' ');
		}
	},

	/* -- privates */

	initState: function() {
		this.defaultState = this.getState();
		this.applyState(StateMgr.get(this.stateId || this.id));
		if(this.stateEvents && Object.isArray(this.stateEvents)) {
			for(var i = 0, l = this.stateEvents.length; i < l; ++i) {
				this.on(this.stateEvents[i], this.saveState, this);
			}
		}
	},

	getActionEl: function() {
		return this.element;
	},

	getResizeEl: function() {
		return this.resizeEl || this.element;
	},

	getPositionEl: function() {
		return this.positionEl || this.element;
	},

	getState: function() {
		return {};
	},

	applyState: function(state) {
		if(!state) return;

		if(state.width !== undefined)
			this.width = state.width;
		if(state.height !== undefined)
			this.height = state.height;
		if(state.options !== undefined) {
			this.setOptions(state.options);
		}
	},

	saveState: function() {
		StateMgr.set(this.stateId || this.id, this.getState());
	},

	onRender: function(container, position) {
		if(this.element) {
			container.insertBefore($(this.element), position);
		}
	},

	afterRender: function() {
		this.boxReady = true;
		if(this.width !== undefined || this.height !== undefined) {
			this.setDimensions(this.width, this.height);
		}
	},

	onDisable: function() {
		this.getActionEl().addClassName(this.disabledClass);
		this.element.disabled = true;
	},

	onEnable: function() {
		this.getActionEl().removeClassName(this.disabledClass);
		this.element.disabled = false;
	},

	onShow: function() {
		if(this.hideParent) {
			this.container.removeClassName('ui-hide-' + this.hideMode);
		} else {
			this.getActionEl().removeClassName('ui-hide' + this.hideMode);
		}
	},

	onHide: function() {
		if(this.hideParent) {
			this.container.addClassName('ui-hide-' + this.hideMode);
		} else {
			this.getActionEl().addClassName('ui-hide' + this.hideMode);
		}
	},

	destroy: function() {
		if(this.fire('BeforeDestroy', this) !== false) {
			this.beforeDestroy();
			if(this.rendered) {
				this.element.off();
				this.element.remove();
			}
			this.onDestroy();
			this.fire('Destroy', this);
			this.off();
			UI.ComponentMgr.unregister(this);
		}
	},

	adjustDimensions: function(w, h) {
		if(this.options.autoWidth) {
			w = 'auto';
		}
		if(this.options.autoHeight) {
			h = 'auto';
		}
		return {width: w, height: h};
	},

	onResize: function(aw, ah, w, h) {
	},

	onSetLeftTop: function(x, y) {
	},

	initComponent: Prototype.emptyFunction,
	beforeDestroy: Prototype.emptyFunction,
	onDestroy: Prototype.emptyFunction
});

UI.Container = Class.create(UI.Component, {
	initialize: function(options) {
		UI.Container.parentClass.initialize.call(this, (Object.extend({
			autoDestroy: false,
			monitorResize: false
		}, options || {})));
	},

	add: function() {
		if(!this.items) {
			this.init();
		}
		for(var i = 0, l = arguments.length; i < l; ++i) {
			var comp = arguments[i];
			var pos = this.items.length;

			if(this.fire('BeforeAdd', this, comp, pos) !== false) {
				if(!Object.isFunction(comp.render)) {
					comp = new UI[comp.component || 'Panel'](comp);
				}
				comp.ownerCt = this;
				this.items.add(comp);
				this.fire('Add', this, comp, pos);
			}
		}
		return comp;
	},

	remove: function(comp, autoDestroy) {
		var c = this.getComponent(comp);
		if(c && this.fire('BeforeRemove', this, c) !== false) {
			this.items.remove(c);
			delete c.ownerCt;
			if(autoDestroy === true || (autoDestroy !== false && this.options.autoDestroy)) {
				c.destroy();
			}
			if(this.layout && this.layout.activeItem == c) {
				delete this.layout.activeItem;
			}
			this.fire('Remove', this, c);
		}
	},

	render: function(container) {
		UI.Container.parentClass.render.call(this, container);
		var opts = this.options;
		if(opts.layout) {
			this.layout = Object.isString(opts.layout) ? this.layout = new UI.Layout[opts.layout](opts.layoutOptions) : opts.layout
			this.setLayout(this.layout);
			delete opts.layout;
			delete opts.layoutOptions;
		}
		if(!this.ownerCt) {
			this.doLayout(); // at root, do layout
		}
		if(this.options.monitorResize === true) {
			Event.observe(window, 'resize', this.doLayout, this);
		}
	},

	getComponent: function(comp) {
		return (typeof comp == 'object') ? comp : this.items.get(comp);
	},

	getLayout : function() {
		if(!this.layout) {
			var layout = new UI.Layout.Container(this.layoutOptions);
			this.setLayout(layout);
		}
		return this.layout;
	},

	setLayout: function(layout) {
		if(this.layout && this.layout != layout) {
			this.layout.setContainer(null);
		}
		this.init();
		this.layout = layout();
		layout.setContainer(this);
	},

	/* -- privates */

	initComponent: function() {
		var items = this.options.items;
		if(items) {
			if(Object.isArray(items)) {
				this.add.apply(this, items);
			} else {
				this.add(items);
			}
			delete this.options.items;
		}
	},

	init: function() {
		if(!this.items) {
			this.items = new Collection();
			this.getLayout();
		}
	},

	setLayout: function(layout){
		if(this.layout && this.layout != layout) {
			this.layout.setContainer(null);
		}
		this.init();
		this.layout = layout;
		layout.setContainer(this);
	},

	// tell all items to do its layout
	doLayout: function() {
		if(this.rendered && this.layout) {
			this.layout.layout();
		}
		if(this.items) {
			this.items.each(function(item) {
				if(item.doLayout) {
					item.doLayout();
				}
			}, this);
		}
	},

	// returns the element in this container that other layouts should render into.
	getLayoutTarget: function() {
		return this.element;
	}
});

UI.Shadow = Class.create(Class.baseMethods, {
	initialize: function(options) {
		this.setOptions({
			offsetX: 4,
			offsetY: 4,
			offsetWidth: 0,
			offsetHeight: 0
		}, options || {});

		if(Prototype.Browser.IE) {
			this.options.offsetWidth -= 5;
			this.options.offsetHeight -= 5;
		}
	},

	show: function(target, options) {
		this.setOptions(options);
		if((target = $(target))) {
			if(!this.element) {
				this.element = UI.Shadow.Pool.pull();
				if(this.element.nextSibling != target) {
					target.parentNode.insertBefore(this.element, target);
				}
			}
			this.element.style.zIndex = (parseInt(target.getStyle('zIndex'), 10) || 1) - 1;
			if(Prototype.Browser.IE) {
				this.element.style.filter = "progid:DXImageTransform.Microsoft.alpha(opacity=90) progid:DXImageTransform.Microsoft.Blur(pixelradius=2)";
			}
			var dim = target.getDimensions();
			var lt = target.getLeftTop();
			this.align(lt.left, lt.top, dim.width, dim.height);
			this.element.style.display = 'block';
		}
	},

	hide: function() {
		if(this.element) {
			this.element.hide();
			UI.Shadow.Pool.push(this.element);
			delete this.element;
		}
	},

	align: function(x, y, w, h) {
		if(!this.element) {
			return;
		}
		var o = this.options, es = this.element.style,
			isIE = Prototype.Browser.IE,
			sw = w + o.offsetWidth, sws = sw + 'px',
			sh = h + o.offsetHeight, shs = sh + 'px';
		this.element.setLeftTop(x + o.offsetX, y + o.offsetY);
		if(es.width != sws || es.height != shs) {
			es.width = sws;
			es.height = shs;
			if(!isIE) {
				var cn = this.element.childNodes;
				var cw = Math.max(0, w - 10) + 'px';
				cn[0].childNodes[1].style.width = cw;
				cn[1].childNodes[1].style.width = cw;
				cn[2].childNodes[1].style.width = cw;
				cn[1].style.height = Math.max(0, sh - 10) + 'px';
			}
		}
	},

	setZIndex: function(z) {
		this.zIndex = z;
		if(this.element) {
			this.element.style.zIndex = z;
		}
	},

	isVisible: function() {
		return !!this.element;
	}
});

UI.Shadow.Pool = (function() {
	var pool = [];

	return {
		pull: function() {
			var sh = pool.shift();
			if(!sh) {
				sh = $(document.body).createChild('div', {className: Prototype.Browser.IE ? 'ui-ie-shadow' : 'ui-shadow'});
				if(!Prototype.Browser.IE) {
					sh.update(
						'<div class="st"><div class="stl"></div><div class="stc"></div><div class="str"></div></div>'
						+ '<div class="sm"><div class="sml"></div><div class="smc"></div><div class="smr"></div></div>'
						+ '<div class="sb"><div class="sbl"></div><div class="sbc"></div><div class="sbr"></div></div>'
					);
				}
			}
			return sh;
		},

		push: function(sh) {
			pool.push(sh);
		}
	};
})();

UI.Layer = Class.create(UI.Element, Class.baseMethods, {
	visible: true,

	initialize: function(options) {
		this.setOptions({
			className: '',
			shadow: true,
			zIndex: 200,
			shadowOptions: {},
			useDisplay: false
		}, options || {});

		var o = this.options;

		if(o.element && $(o.element)) {
			this.element = this.options.element;
			this.element.addClassName('ui-layer');
			delete o.element;
		} else {
			this.element = $(document.body).createChild('div', {className : 'ui-layer'});
		}
		this.element.addClassName(o.className);
		this.element.style.zIndex = o.zIndex;
		if(o.shadow) {
			this.shadow = new UI.Shadow(this.shadowOptions);
		}
		this.hide();
	},

	getZIndex: function() {
		return this.zindex || parseInt(this.element.getStyle('zIndex'), 10) || 200;
	},

	show: function() {
		this.visible = true;
		if(this.options.useDisplay) {
			this.element.show();
		} else {
			if(this.lastLT) {
				this.element.setLeftTop(this.lastLT);
			}
			this.element.makeVisible();
		}
		if(this.shadow) {
			this.shadow.show(this.element, this.shadowOptions);
		}
		return this;
	},

	hide: function() {
		this.visible = false;
		if(this.options.useDisplay) {
			this.element.hide();
		} else {
			this.element.setLeftTop(-10000, -10000).makeInvisible();
		}
		if(this.shadow) {
			this.shadow.hide();
		}
		return this;
	},

	insert: function(html) {
		this.element.insert(html);
		this.sync();
		return this;
	},

	update: function(html) {
		this.element.update(html);
		this.sync();
		return this;
	},

	isVisible: function() {
		return this.visible;
	},

	setXY: function(x, y) {
		if(x && typeof x == 'object') {
			y = x.y;
			x = x.x;
		}
		if(x === undefined) x = this.getX();
		if(y === undefined) y = this.getY();
		this.lastLT = this.element.translatePoints(x, y);
		this.element.setLeftTop(this.lastLT);
		this.sync();
		return this;
	},

	setLeftTop: function(l, t) {
		if(l && typeof l == 'object') {
			t = l.top;
			l = l.left;
		}
		if(l === undefined) l = this.element.getLeft();
		if(t === undefined) t = this.element.getTop();
		this.lastLT = {left: l, top: t};
		this.element.setLeftTop(l, t);
		this.sync();
		return this;
	},

	/* -- privates */

	destroy: function() {
		if(this.shadow) {
			this.shadow.hide();
		}
		this.off();
		this.element.off().remove();
	},

	sync: function(show) {
		var sw = this.shadow;
		var lt = this.getLeftTop();
		var wh = this.getDimensions();
		if(sw && this.shadowDisabled !== true) {
			if(show && !sw.isVisible()) {
				sw.show(this);
			} else {
				sw.align(lt.left, lt.top, wh.width, wh.height);
			}
		}
	}
});

UI.LoadMask = Class.create(Class.baseMethods, {
	disabled: false,

	initialize: function(element, options){
		this.element = $(element);

		if(options.store){
			this.store = options.store;
			delete options.store;
			this.store.on({
				BeforeLoad: this.onBeforeLoad,
				Load: this.onLoad,
				loadFailure: this.onLoad
			}, this);
		}

		this.setOptions({
			removeMask: false,
			msg: 'Loading...',
			msgCls: 'ui-mask-loading'
		}, options);
	},

	disable : function() {
	   this.disabled = true;
	},

	enable : function() {
		this.disabled = false;
	},

	show: function() {
		this.onBeforeLoad();
	},

	hide: function() {
		this.onLoad();
	},

	/* -- privates */

	onLoad : function() {
		this.element.unmask(this.options.removeMask);
	},

	onBeforeLoad : function() {
		if(!this.disabled){
			this.element.mask(this.options.msg, this.options.msgCls);
		}
	},

	destroy : function() {
		if(this.store){
			this.store.off({
				BeforeLoad: this.onBeforeLoad,
				Load: this.onLoad,
				loadFailure: this.onLoad
			});
		}
	}
});


UI.Tip = Class.create({
	initialize: function(target, options) {
		this.target = $(target);
		this.setOptions({
			zIndex: 200,
			text: false,
			interceptTitle: true,
			followMouse: false,
			mouseOffset: [0,18],
			autoHide: true,
			dismissDelay: 0,
			showDelay: 500,
			hideDelay: 100
		}, options || {});

		this.initTarget();
		this.showBound = this.show.bind(this);
		this.hideBound = this.hide.bind(this);

		this.hidden = true;
	},

	show: function(delay) {
		this.clearTimers();
		if(delay) {
			this.showTimer = setTimeout(this.showBound, delay, 0);
		} else {
			this.clearTimers();
			this.showAt(this.getPointer());
		}
	},

	delayShow: function() {
		if(this.hidden && this.lastShown !== undefined && (Date.time() - this.lastShown) <= 250) {
			this.show();
		} else if(!this.hidden) {
			this.show();
		} else {
			this.show(this.options.showDelay);
		}
	},

	showAt: function(xy) {
		var text = this.options.text;
		if(text) {
			this.getLayer();
			this.layer.element.update(text);
		}
		if(this.layer) {
			this.layer.setXY(xy);
			if(this.hidden) {
				this.layer.show();
				this.hidden = false;
				this.onShow();
				this.lastShown = Date.time();
			}

			var d = this.options.dismissDelay;
			if(d && this.options.autoHide !== false) {
				this.clearTimer('dismiss');
				this.dismissTimer = this.hide.bind(this).delayMS(d);
			}
		}
	},

	hide: function(delay) {
		this.clearTimers();
		if(delay) {
			this.hideTimer = setTimeout(this.hideBound, delay, 0);
		} else {
			if(this.layer) {
				this.hidden = true;
				this.layer.hide();
			}
			this.onHide();
		}
	},

	delayHide: function() {
		this.hide(this.options.hideDelay);
	},

	setText: function(text) {
		this.options.text = text;
	},

	getText: function(text) {
		return this.options.text;
	},

	getLayer: function() {
		if(!this.layer) {
			this.layer = new UI.Layer({
				zIndex: this.options.zIndex,
				className: 'ui-tip'
			});
		}
		return this.layer;
	},

	/* privates -- */

	initTarget: function() {
		if(this.target) {
			this.target.on({
				mouseover: this.onMouseOver,
				mouseout: this.onMouseOut,
				mousemove: this.onMouseMove
			}, this);
		}
	},

	onShow: function() {
		document.on('mousedown', this.onDocMouseDown, this);
	},

	onHide: function() {
		document.off('mousedown', this.onDocMouseDown, this);
	},

	clearTimer: function(n) {
		n = n + 'Timer';
		clearTimeout(this[n]);
		delete this[n];
	},

	clearTimers: function(timer) {
		this.clearTimer('show');
		this.clearTimer('hide');
		this.clearTimer('dismiss');
	},

	getPointer: function() {
		var o = this.options.mouseOffset || [0,0];
		return {
			x: this.pointer.x + o[0],
			y: this.pointer.y + o[1]
		};
	},

	onMouseOver: function(event) {
		if(event.capture(this.target)) {
			this.pointer = event.pointer();
			this.delayShow();
		}
	},

	onMouseOut: function(event) {
		if(!event.capture(this.target)) {
			return;
		}
		if(!this.hidden && this.layer && this.options.autoHide) {
			this.clearTimers();
			if(this.layer.isVisible()) {
				this.delayHide();
			}
		}
	},

	onMouseMove: function(event) {
		this.pointer = event.pointer();
		if(this.layer && this.layer.isVisible() && this.options.followMouse) {
			this.layer.setXY(this.getPointer());
		}
	},

	onDocMouseDown: function(event) {
		if(this.options.autoHide && !event.capture(this.target)) {
			this.hide();
		}
	}
});

// tooltip delegation
UI.DTip = Class.create(UI.Tip, {

	/* -- privates */

	onMouseOver: function(event) {
		var o = this.options, cfg = this.tagConfig;
		this.pointer = event.pointer();
		var t = event.element();
		if(!t || t.nodeType !== 1 || t == this.target) {
			return;
		}
		var text = this.extractText(t, event);
		if(text) {
			this.setText(text);
			this.delayShow();
		}
	},

	/*
	  this function is called on every mouseover/out event on an element
	  and should return the tip text. This should be overridden.
	*/
	extractText: function(el, event) {
		return el.title || el.innerHTML;
	}
});

UI.QTip = Class.create(UI.DTip, {
	tagConfig : {
		namespace: 'ui',
		attribute: 'qtip',
		width: 'qwidth',
		target: "target",
		title: 'qtitle',
		hide: 'hide',
		cls: 'qclass',
		align: 'qalign'
	},

	initialize: function(options) {
		options = Object.extend({
			interceptTitle: false
		}, options || {});
		UI.QTip.parentClass.initialize.call(this, document, options);
		this.targets = {};
	},

	register: function(o) {
		var opts = Object.isArray(o) ? o : arguments;
		for(var i = 0, l = opts.length; i < l; ++i) {
			var c = opts[i];
			var t = c.target;
			if(t) {
				if(Object.isArray(t)) {
					for(var j = 0, k = t.length; j < k; ++j) {
						this.targets[Element.identify(Element.get(t[j]))] = c;
					}
				} else {
					this.targets[Element.identify(Element.get(t))] = c;
				}
			}
		}
	},

	unregister: function(el) {
		delete this.targets[Element.identify(el)];
	},

	showAt: function(xy) {
		var t = this.activeTarget;
		if(t) {
			Object.extend(this.options, {
				text: t.text,
				autoHide: t.autoHide
			});
			if(t.align) {
				xy = this.layer.element.getAlignToXY(t.el, t.align);
			}
		}
		UI.QTip.parentClass.showAt.call(this, xy);
	},

	/* -- privates */

	extractText: function(t, event) {
		if(t == document || t == document.body) {
			return false;
		}
		if(this.activeTarget && t == this.activeTarget.el) {
			this.show();
			return false;
		}
		if(t && this.targets[t.id]) {
			this.activeTarget = this.targets[t.id];
			this.activeTarget.el = t;
			this.delayShow();
			return false;
		}

		var o = this.options, cfg = this.tagConfig;
		var text, ns = cfg.namespace;

		if(o.interceptTitle && t.title) {
			text = t.title;
			t.qtip = text;
			t.removeAttribute('title');
			event.preventDefault();
		} else {
			text = t.qtip || t.readAttributeNS(ns, cfg.attribute);
		}

		if(text) {
			var autoHide = t.readAttributeNS(ns, cfg.hide);
			this.activeTarget = {
				el: t,
				text: text,
				width: t.readAttributeNS(ns, cfg.width),
				autoHide: (autoHide ? autoHide !== 'false' : this.options.autoHide),
				title: t.readAttributeNS(ns, cfg.title),
				cls: t.readAttributeNS(ns, cfg.cls),
				align: t.readAttributeNS(ns, cfg.align) || this.options.align
			};
		}
		return text;
	},

	onMouseOut: function() {
		if(this.options.autoHide !== false) {
			this.delayHide();
			delete this.activeTarget;
		}
	}
});

UI.Quicktip = (function() {
	var tip;

	return {
		init: function(options) {
			if(!tip) {
				tip = new UI.QTip();
			}
			tip.setOptions(options);
		},

		register: function() {
			tip.register.apply(tip, arguments);
		},

		unregister: function() {
			tip.register.apply(tip, arguments);
		},

		getTip: function() {
			return tip;
		}
	};
}());


