/*
 * Aurora - v0.1
 * Copyright (c) 2009, Ryan Morr (ryanmorr.com)
 * Licensed under the MIT license.
 */

(function(){ 

this.Aurora = {
	
	/**
	 * Contains all declared data types defined by Aurora.define
	 * @type Object
	 */
	types: {},
	
	/**
	 * Contains all declared classes using Aurora.declare()
	 * @type Object
	 */
	classes: {},
	
	/**
	 * Register a class to be used as a data type
	 * @param {String} name The name of the class
     * @param {Function} klass The class being registered
	 */
	register: function(name, klass){
		Aurora.classes[name] = klass;
		Aurora.define(name, function(o){
			return Aurora.isClassInstance(o, klass);						  
		}, klass);
	},
	
	/**
	 * Define a data type
	 * @param {String} name The name of the data type
     * @param {Function} fn The fn used to validate acceptable variable assignments
	 */
	define: function(name, fn, obj){
		var type = Aurora.namespace(name, obj);
		type._name = name;
		type._isValid = fn;		
		Aurora.types[name] = type;
	},
	
	/**
	 * Define a namespace on the global object
	 * @param {String} namespace The namespace to be created
     * @param {Object || Function} obj The object assignment for the namespace
	 */	
	namespace: function(ns, obj){
		obj = obj || {};
		var parts = ns.split(".");
        var root = window[parts[0]] = window[parts[0]] || obj;
		if(parts.length > 1){
			for (var i=1; i < parts.length; i++) {
				root[parts[i]] = root[parts[i]] || ((i == (parts.length-1)) ? obj : {});
				root = root[parts[i]];
			}
		}
		return root;
	},
	
	/**
	 * Apply all properties from on object to another, by default only undefined properites of the reciever will be applied
	 * @param {Object} obj The suppling object
     * @param {Object} properties The recieving object
	 * @param {Boolean} override True if defined properties of the reciever should be overridden
	 * @return {Object} The recieving object with the new properties applied
	 */
	mixin: function(obj, properties, override){
		for(var prop in properties){
			if(override || Aurora.isUndefined(obj[prop])){
				obj[prop] = properties[prop];
			}
		}
		return obj;
	},
	
	/**
	 * Provide simple inheritance between two classes
	 * @param {Function} subclass The subclass of which the prototype chain of the superclass and the overrides will be applied to
     * @param {Function} superclass The superclass from which the subclass will inherit from
	 * @param {Object} properties All properties to be applied to the subclass and override any existing properties
	 */
	extend: function(subclass, superclass, overrides){
		var fn = function(){};		
		fn.prototype = superclass.prototype;
		subclass.prototype = new fn();
		subclass.prototype.constructor = subclass;
		subclass.superclass = superclass.prototype;
		Aurora.mixin(subclass.prototype, overrides, true);
	},
	
	/**
	 * Declare a new class, allows for class inheritance and defines the class as a data type
	 * @param {String} className The name of the new class
     * @param {Function} superclass The superclass from which the new class will inherit from. This parameter is optional, 
	   	 	  if omitted, properties can be supplied as the second parameter
	 * @param {Object} properties All methods and properties to apply to the new class
	 */
	declare: function(className, superclass, properties){	
		var klass = function(){
			this._init.apply(this, arguments);
		};
		
		if(Aurora.isObject(superclass)){
			properties = superclass;
			properties._props = {}; 
			properties.invariant = properties.invariant || {};
		}else{
			properties._props = {}; 
			properties.invariant = properties.invariant || {};
			Aurora.mixin(properties._props, superclass.prototype._props);
			Aurora.mixin(properties.invariant, superclass.prototype.invariant);
			Aurora.extend(klass, superclass, properties);
		}
		
		for(var prop in properties){
			if(prop != 'constructor' && Aurora.isFunction(properties[prop]) && !Aurora.isType(properties[prop])){
				properties[prop] = Aurora.intercept(properties[prop], prop);
			}else if(prop != 'invariant'){
				if(Aurora.isType(properties[prop])){
					properties._props[prop] = properties[prop]; 
				}
			}
		}		
		
		Aurora.mixin(klass.prototype, properties, true);
		klass.prototype._className = className;
		klass.prototype._init = Aurora.intercept(properties.constructor || function(){}, 'constructor');
		Aurora.register(className, klass);
	},
	
	/**
	 * Intercepts all methods of an instance to perform type and invariant checking, instance interceptions 
	   can only take place one at a time, if a violation is found, it is added to a stack which will be recursively 
	   called after all data checking is complete.
	 * @param {Function} method The class method to be intercepted
	 * @param {Function} name The name of the method to be intercepted
	 * @return {Function} The new method used for interception
	 */
	intercept: function(method, name){
		return function(){
			if(!this._intercepted){
				var instance = this;
				instance._intercepted = true;
				var result = method.apply(this, arguments);
				for(var prop in instance){
					if(!Aurora.isFunction(instance[prop])){
						var value = instance[prop];
						var type = instance._props[prop];
						if(type){
							var typeEx = Aurora.typeCheck(value, type, prop, instance, name);
							if(!typeEx){
								var invariantEx = Aurora.invariantCheck(prop, instance, name);
								if(invariantEx){
									throw invariantEx;
								}
							}else{
								throw typeEx;
							}
						}
					}
				}
				
				instance._intercepted = false;
				return result;
			}else{
				return method.apply(this, arguments);
			}
		}
	},
	
	/**
	 * Performs an type validation on a property of a class
	 * @param {Unkown} value The current value of the supplied property
	 * @param {Object} type The class method to be intercepted
	 * @param {String} prop The property being type checked
	 * @param {Function} instance The class to which prop belongs
	 * @return {Boolean} Was a violation found
	 */
	typeCheck: function(value, type, prop, instance, method){
		if(Aurora.isArray(type)){
			if(Aurora.isType(type[0])){
				if(!Aurora.isValidArray(value, type[0])){
					return Aurora.Exception.array(instance._className, method, prop, type[0]._name);
				}
				return false;
			}else{
				if(!Aurora.isArray(value)){
					return Aurora.Exception.type(instance._className, method, prop, 'Array');
				}
				return false;
			}
		}else{
			if(!type._isValid(value)){
				return Aurora.Exception.type(instance._className, method, prop, type._name);
			}
			return false;
		}
	},	
	
	/**
	 * Performs an invariant validation on a property of a class
	 * @param {String} prop The property being validated
	 * @param {Function} instance The class to which prop belongs
	 * @return {Boolean} Was a violation found
	 */
	invariantCheck: function(prop, instance, method){
		var invariant = instance.invariant[prop];
		if(invariant && !invariant.call(instance)){
			return Aurora.Exception.invariant(instance._className, method, prop);
		}
		return false;
	},
		
	/**
	 * Is the object a valid DOM element
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isElement: function(o){
		return (o.htmlElement || (o.nodeName && o.nodeType === 1)) ? true : false;
	},
	
	/**
	 * Is the object a browser event
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isEvent: function(o){
		return (window.Event && o instanceof Event) || (Aurora.isObject(o) && Aurora.isUndefined(o.constructor) && (window.event && o.clientX && o.clientX === window.event.clientX));
	},
	
	/**
	 * Is the object a hash
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isHash: function(o){
		return Aurora.isObject(o) && o.constructor === Object && !Aurora.isEvent(o) && !Aurora.isFunction(o);
	},
	
	/**
	 * Is the object an array
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isArray: function(o){ 
		return Object.prototype.toString.call(o) === "[object Array]";
    },
	
	/**
	 * Does the object display array-like behaviour (arguments, nodeList)
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isArrayLike: function(o){
		return (Aurora.isNumber(o.length) && !Aurora.isFunction(o) && !Aurora.isString(o)) || Aurora.isArray(o) || Aurora.isNodeList(o);
	},
	
	/**
	 * Is the object a boolean
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
    isBoolean: function(o){
        return typeof o === 'boolean';
    },
	
	/**
	 * Is the supplied object a function
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
    isFunction: function(o){
		return Object.prototype.toString.call(o) === "[object Function]";
    },
	
	/**
	 * Is the object null
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
    isNull: function(o){
        return o === null;
    },
	
	/**
	 * Is the object undefined
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isUndefined: function(o){
        return typeof o === 'undefined';
    },
	
	/**
	 * Is the object undefined or null
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isVoid: function(o){
        return Aurora.isNull(o) || Aurora.isUndefined(o);
    },
	
	/**
	 * Is the object a number
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
    isNumber: function(o){
        return typeof o === 'number' && isFinite(o);
    },
	
	/**
	 * Is the object a number and decimal
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isDecimal: function(o){
		return (/^-?(\d+|(\d*[.,]\d+))$/).test(String(o)) && Aurora.isNumber(o);
	},
	
	/**
	 * Is the object a number and integer
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isInteger: function(o){
		return (/^-?\d+$/).test(String(o)) && Aurora.isNumber(o);
	},
      
	 /**
	 * Is the object an object
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */ 
    isObject: function(o){
		return typeof o === 'object';
    },
       
	/**
	 * Is the object a string
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
    isString: function(o){
        return typeof o === 'string';
    },
	
	/**
	 * Is the object a date
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isDate: function(o){
		return o instanceof Date;
	},
	
	/**
	 * Is the object a regular expression
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isRegExp: function(o){
		return Object.prototype.toString.call(o) === '[object RegExp]';
	},
	
	/**
	 * Is the an XML document or XML node
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isXML: function(o){
		return Aurora.isXMLDocument(o) || Aurora.isXMLNode(o);
	},
	
	/**
	 * Is the object an XML document
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isXMLDocument: function(o){
		return !!(o.documentElement && !o.body);
	},
	
	/**
	 * Is the object an XML node
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isXMLNode: function(o){
		return o.tagName && o.ownerDocument && !o.ownerDocument.body ? true : false;
	},
	
	/**
	 * Is the object a document
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isDocument: function(o){
		return Object.prototype.toString.call(o) === '[object HTMLDocument]' || o.nodeType === 9 || Aurora.isXMLDocument(o);
    },
		
	/**
	 * Is the object a native function to the browser
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isNative: function(o){
		return (/\{\s*\[native code\]\s*\}/).test(o.toString());
	},
	
	/**
	 * Is the object a valid node (HTML/XML)
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isNode: function(o){
		return (o.nodeName && o.nodeType && (/1|2|3|4|5|6|7|8|9|10|11|12/).test(o.nodeType)) || Aurora.isXML(o)
	},
		
	/**
	 * Is the object a valid node list
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isNodeList: function(o){
		var type = Object.prototype.toString.call(o);
		if(type === "[object NodeList]" || type === "[object HTMLCollection]"){
			return true;
		}else if(Aurora.isArrayLike(o)){
			for(var i=0; i < o.length; i++){
				if(!Aurora.isNode(o[i])){
					return false;
				}
			}
			return true;
		}else{
			return false;
		}
	},
	
	/**
	 * Is the object a browser window object
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isWindow: function(o){
		var type = Object.prototype.toString.call(o);
		if(type ==="[object Window]" || type ==="[object DOMWindow]" || type ==="[object global]"){
			return true;
		}else if(Aurora.isObject(o) && !!o.Array){
			return true;
		}else{
			return false;
		}
    },
	
	/**
	 * Is the object a registered class with Aurora
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isClass: function(o){
		return Aurora.isString(o) ? !!Aurora.classes[o] : (Aurora.isFunction(o) && !!Aurora.classes[o._name]);
	},
	
	/**
	 * Is the object an instance of the supplied registered class
	 * @param {Unknown} o The object being tested
	 * @param {Function} klass The class which 0 will be compared with
	 * @return {Boolean} The result
	 */
	isClassInstance: function(o, klass){
		return Aurora.isClass(klass) && o instanceof klass;
	},
	
	/**
	 * Is the object a valid data type of Aurora
	 * @param {Unknown} o The object being tested
	 * @return {Boolean} The result
	 */
	isType: function(o){
		if(Aurora.isArray(o)){
			return o[0] && Aurora.isType(o[0]);
		}else{
			return !!Aurora.types[o._name];
		}
	},
	
	/**
	 * Is the object an array and does it contain only items of the supplied data type
	 * @param {Unknown} o The object being tested
	 * @param {Object} rule The data type which all items of o must adhere to
	 * @return {Boolean} The result
	 */
	isValidArray: function(o, rule){
		if(Aurora.isArray(o)){
			if(rule){
				for(var i=0; i < o.length; i++){
					if(!rule._isValid(o[i])){
						return false;
					}
				}
			}
			return true;
		}else{
			return false;
		}
	}
};



Aurora.Exception = {
	/**
	 * Create a new array type exception
	 * @param {String} className The name of the class in which threw the exception
	 * @param {String} prop The name of the invalid property
	 * @return {String} type The declared type of the property
	 */
	array: function(className, method, prop, type){
		return new Error('Type Violation on '+className+' in '+method+': array items of instance variable "'+prop+'" must match the expected type ('+type+')');
	},
	
	/**
	 * Create a new type exception
	 * @param {String} className The name of the class in which threw the exception
	 * @param {String} prop The name of the invalid property
	 * @return {String} type The declared type of the property
	 */
	type: function(className, method, prop, type){
		return new Error('Type Violation on '+className+' in '+method+': instance variable "'+prop+'" must match the expected type ('+type+')');
	},
	
	/**
	 * Create a new invariant exception
	 * @param {String} className The name of the class in which threw the exception
	 * @param {String} prop The name of the invalid property
	 */
	invariant: function(className, method, prop){
		return new Error('Invariant Violation on '+className+' in '+method+': instance variable "'+prop+'" must meet the terms of the invariant');
	}
};



/**
 * Define the native data types of Aurora
 */
var types = ['String', 'Date', 'Number', 'RegExp', 'Function', 'Boolean', 'Array', 'Object', 'Null', 'Element', 'Event', 'NodeList'];
for(var i=0; i < types.length; i++){
	var type = types[i];
	Aurora.define(type, Aurora['is'+type]);
};

}())
