/*
Class ported and modified by Scott Kyle from:
	MooTools - My Object Oriented JavaScript Tools.

License:
	MIT-style license.

Copyright:
	Copyright (c) 2006-2009 [Valerio Proietti](http://mad4milk.net/).

Code & Documentation:
	[From the MooTools Documentation](http://mootools.net/docs/core/Class/Class).

Inspiration:
	- Class implementation inspired by [Base.js](http://dean.edwards.name/weblog/2006/03/base/) Copyright (c) 2006 Dean Edwards, [GNU Lesser General Public License](http://opensource.org/licenses/lgpl-license.php)
	- Some functionality inspired by [Prototype.js](http://prototypejs.org) Copyright (c) 2005-2007 Sam Stephenson, [MIT License](http://opensource.org/licenses/mit-license.php)
*/

(function(){

// UTILITY FUNCTIONS

var $empty = function(){};

var $type = function(obj){
	if (obj == undefined) return false;
	var type = typeof obj;
	
	if (type == 'number' && !isFinite(obj)) return false;
	
	if (typeof obj.length == 'number') {
		if (obj instanceof Array) return 'array';
		else if (obj.callee) return 'arguments';
		else if (obj.item) return 'collection';
	}
	
	return type;
};

var $unlink = function(object){
	var unlinked;
	switch ($type(object)){
		case 'object':
			unlinked = {};
			for (var p in object) unlinked[p] = $unlink(object[p]);
		break;
		case 'array':
			unlinked = [];
			for (var i = 0, l = object.length; i < l; i++) unlinked[i] = $unlink(object[i]);
		break;
		default: return object;
	}
	return unlinked;
};

var $splat = function(obj){
	var type = $type(obj);
	return (type) ? ((type != 'array' && type != 'arguments') ? [obj] : obj) : [];
};

var $merge = function(){
	var args = Array.prototype.slice.call(arguments);
	args.unshift({});
	return $mixin.apply(null, args);
};

var $mixin = function(mix){
	for (var i = 1, l = arguments.length; i < l; i++){
		var object = arguments[i];
		if ($type(object) != 'object') continue;
		for (var key in object){
			var op = object[key], mp = mix[key];
			mix[key] = (mp && $type(op) == 'object' && $type(mp) == 'object') ? $mixin(mp, op) : $unlink(op);
		}
	}
	return mix;
};

var $each = function(iterable, fn, bind){
	var type = $type(iterable);
	if (type == 'arguments' || type == 'collection' || type == 'array'){
		for (var i = 0, l = iterable.length; i < l; i++) fn.call(bind, iterable[i], i, iterable);
	} else {
		for (var key in iterable){
			if (iterable.hasOwnProperty(key)) fn.call(bind, iterable[key], key, iterable);
		}
	}
};


// PRIVATE CLASS FUNCTIONS

var bind = function(self, bind){
	var fn = function(){
		return self.apply(bind, arguments);
	};
	
	fn._bound_ = self;
	return fn;
};

var resetAll = function(object, key){
		
	if (key == null){
		for (var p in object) resetAll(object, p);
		return object;
	}
	
	delete object[key];
	
	switch ($type(object[key])){
		case 'object':
			var F = function(){};
			F.prototype = object[key];
			var i = new F;
			object[key] = resetAll(i);
		break;
		case 'array': object[key] = $unlink(object[key]); break;
	}
	
	return object;
	
};

var instantiate = function(F){
	F._prototyping_ = true;
	var proto = new F;
	delete F._prototyping_;
	return proto;
};

var wrap = function(self, key, method){
	if (method._origin_) method = method._origin_;
	
	var fn = function(){
		if (method._protected_ && this._current_ == null) throw new Error('The method "' + key + '" cannot be called.');
		var caller = this.caller, current = this._current_;
		this.caller = current; this._current_ = arguments.callee;
		var result = method.apply(this, arguments);
		this._current_ = current; this.caller = caller;
		return result;
	};
	
	fn._owner_ = self;
	fn._origin_ = method;
	fn._name_ = key;
	
	return fn;
};


// CLASS

this.Class = function(params){
	
	if (params instanceof Function) params = {initialize: params};
	
	var newClass = function(){
		resetAll(this);
		if (newClass._prototyping_) return this;
		
		// post-init mutators
		for (var modifier in Class.Initializers){
			if (this[modifier])
				this[modifier] = Class.Initializers[modifier].call(this, this[modifier]);
		}
		
		this._current_ = $empty;
		var value = (this.initialize) ? this.initialize.apply(this, arguments) : this;
		delete this._current_; delete this.caller;
		return value;
	};
	
	for (var prop in this)
		newClass[prop] = this[prop];
	
	for (var mutator in Class.Mutators){
		if (!params[mutator]) continue;
		params[mutator] = Class.Mutators[mutator].call(newClass, params[mutator], params);
		if (params[mutator] == null) delete params[mutator];
	}
	
	newClass.implement(params);
	
	newClass.constructor = Class;
	newClass.prototype.constructor = newClass;

	return newClass;
	
};

Class.prototype.implement = function(key, value){
	
	if (typeof key != 'string'){
		if (key instanceof Function) key = instantiate(key);
		for (var prop in key) this.implement(prop, key[prop]);
		return this;
	}
	
	var proto = this.prototype;

	switch ($type(value)){
		
		case 'function':
			if (value._hidden_) return this;
			proto[key] = wrap(this, key, value);
		break;
		
		case 'object':
			var previous = proto[key];
			if ($type(previous) == 'object') $mixin(previous, value);
			else proto[key] = $unlink(value);
		break;
		
		case 'array':
			proto[key] = $unlink(value);
		break;
		
		default: proto[key] = value;

	}
	
	return this;

};

Class.prototype.extend = function(key, value){
	if (typeof key == 'string'){
		this[key] = value;
	} else {
		for (var prop in key) this.extend(prop, key[prop]);
	}
	return this;
};


// CLASS MUTATORS

Class.Mutators = {
	
	Extends: function(parent){
		this.parent = parent;
		this.prototype = instantiate(parent);

		this.implement('parent', function(){
			var name = this.caller._name_, previous = this.caller._owner_.parent.prototype[name];
			if (!previous) throw new Error('The method "' + name + '" has no parent.');
			return previous.apply(this, arguments);
		}.protect());
	},

	Implements: function(items){
		items = $splat(items);
		for (var i = 0; i < items.length; i++) this.implement(items[i]);
	}
	
};

Class.Initializers = {
	
	Binds: function(binds){
		
		if (binds === true){
			for (var name in this){
				if (this[name]._origin_) this[name] = bind(this[name], this);
			}
		} else {
			$splat(binds).each(function(name){
				var original = this[name];
				if (original) this[name] = bind(original, this);
			}, this);
		}
		
	}
	
};


// CLASS REFACTOR (by Aaron Newton)

Class.refactor = function(original, refactors){

	$each(refactors, function(item, name){
		var origin = original.prototype[name];
		if (origin && (origin = origin._origin_) && typeof item == 'function') original.implement(name, function(){
			var old = this.previous;
			this.previous = origin;
			var value = item.apply(this, arguments);
			this.previous = old;
			return value;
		}); else original.implement(name, item);
	});

	return original;

};


// MIXINS

this.Options = new Class({
	
	setOptions: function(){
		var options = [this.options];
		options.push.apply(options, arguments);
		this.options = $merge.apply(null, options);
		if (!this.addEvent) return this;
		
		for (var option in this.options){
			if ($type(this.options[option]) != 'function' || !(/^on[A-Z]/).test(option)) continue;
			this.addEvent(option, this.options[option]);
			delete this.options[option];
		}
		return this;
	}
	
});

var removeOn = function(string){
	return string.replace(/^on([A-Z])/, function(full, first){
		return first.toLowerCase();
	});
};

this.Events = new Class({

	$events: {},

	addEvent: function(type, fn, internal){
		type = removeOn(type);
		if (fn != $empty){
			var fns = this.$events[type] = this.$events[type] || [];
			
			if (fns.indexOf(fn) == -1) fns.push(fn);
			if (internal) fn.internal = true;
		}
		return this;
	},

	addEvents: function(events){
		for (var type in events) this.addEvent(type, events[type]);
		return this;
	},

	fireEvent: function(type, args){
		type = removeOn(type);
		if (!this.$events || !this.$events[type]) return this;
		
		args = $splat(args);
		var fns = this.$events[type];
		
		for (var i = 0, l = fns.length; i < l; i++)
			fns[i].apply(this, args);

		return this;
	},

	removeEvent: function(type, fn){
		type = removeOn(type);
		var fns = this.$events[type];
		if (!fns || fn.internal) return this;
		
		for (var i = fns.length; i--; ){
			if (fns[i] === fn) fns.splice(i, 1);
		}
		return this;
	},

	removeEvents: function(events){
		var type;
		if ($type(events) == 'object'){
			for (type in events) this.removeEvent(type, events[type]);
			return this;
		}
		if (events) events = removeOn(events);
		for (type in this.$events){
			if (events && events != type) continue;
			var fns = this.$events[type];
			for (var i = fns.length; i--; i) this.removeEvent(type, fns[i]);
		}
		return this;
	}

});

this.Chain = new Class({

	$chain: [],

	chain: function(){
		this.$chain.push.apply(this.$chain, arguments);
		return this;
	},

	callChain: function(){
		return (this.$chain.length) ? this.$chain.shift().apply(this, arguments) : false;
	},

	clearChain: function(){
		this.$chain.length = 0;
		return this;
	}

});


// ALLOW PASSAGE OF OBJECT INTO JQUERY

if (jQuery){
	var jInit = jQuery.fn.init;
	
	jQuery.fn.init = function(selector){
		if ($type(selector) == 'object'){
			if (selector.toElement) return jInit.call(this, selector.toElement());
			if (selector.element)   return jInit.call(this, selector.element);
		}
		return jInit.apply(this, arguments);
	};
	
	jQuery.fn.init.prototype = jInit.prototype;
}


})();


