/**
 * jsClass
 * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com
 * Licensed under BSD (LICENSE.txt)
 * Date: 6/10/2008
 * @projectDescription Light and extensible Base Class for Javascript.
 * @version 1.0.0 RC1
 * @author Ariel Flesler
 * @url http://flesler.blogspot.com/2008/06/jsclass.html
 */

function Class( extensions, ctor ){
	if( this instanceof Class )// calling new Class();
		return this;
	
	var data = {
		c : ctor, // Constructor
		m : new Class, // Members, all classes really inherit from Class
		s : Class.copy( { }, Class.staticsList ), // Statics
		a : Class.after.concat() // After (callbacks)
	};
		
	if( !ctor )
		data.c = extensions;
	else
		for( var name in extensions )
			if( Class._exts_[name] )
				Class._exts_[name]( data, extensions[name] );
	
	data.m.clazz = data.c;
	data.c.prototype = data.m;
	Class.copy( data.c, data.s );
	
	var i = 0;
	while( data.a[i] )
		data.a[i++]( data );
	
	return data.c;
};

/**
 * Copies the properties from any amount of objects to a source object
 * @param {Object, Function} src Source object that will receive all the attributes.
 * @param {Object} ... The objects from where to take the attributes.
 * @returns {Object, Function} the src element.
 */
Class.copy = function( src ){
	for( var i = 1, l = arguments.length, obj; i < l; )
		if( obj = arguments[i++] )
			for( var attr in obj )
				src[attr] = obj[attr];
	return src;
};

Class.copy( Class, {
	// multi-use getter/setter
	generic:function( obj, key, value ){
		if( value !== undefined )
			obj[key] = value;
		if( typeof key == 'string' )
			return obj[key];
		if( !key )
			return obj;
		Class.copy( obj, key );
	},
	// allows addition of new extension for class creating
	extensions:function( key, value ){
		return Class.generic( Class._exts_, key, value );
	},
	// create a new instance from a function or a prototype, without actually calling the constructor
	instantiate:function( fn ){
		var f = function(){};
		f.prototype = fn.prototype || fn;
		return new f;
	},
	// all the items found in this map, are copied to every class
	staticsList:{
		members:function( key, value ){
			return Class.generic( this.prototype, key, value );
		},
		statics:function( key, value ){
			return Class.generic( this, key, value );
		}
	},
	// callback functions can be added to this array, and will be called for each created class
	after:[ ],
	// internal
	_exts_:{ },
	// all the items found in this map, can be used by any instance.
	prototype:{ }
});

Class(Class); // Class is a class too.

/** 
 * Enables you to create abstract classes, that can be inherited but not instantiated.
 * @author Ariel Flesler
 */
 
Class.extensions( 'abstract', function( data, parse ){
	if( !parse )
		return;
		
	var old = data.c;
	
	data.c = function(){
		if( this.clazz == data.c )// Didn't inherit
			throw new Error("abstract classes cannot be instantiated");
		return old.apply(this,arguments);
	};
});

/** 
 * Enables you to make classes inherit from base classes.
 * @author Ariel Flesler
 */

// 'base' must be the base class constructor
Class.extensions( 'extend', function( data, base ){
	data.m = Class.copy( Class.instantiate(base), data.m );
	data.m.base = data.s.base = base;
});

/** 
 * Handy shortcut to create getters and setters.
 * @author Ariel Flesler
 */

/**
 * Ways to specify the list of getters/setters:
 *	@example { getset: 'name, age, gender' }
 *	@desc A comma separated list of names. The private attribute needs to have that name.
 *
 *	@example { getset: ['name', 'age', 'gender'] }
 *	@desc An array of names. The private attribute needs to have that name.
 *
 *	@example { getset: { name:'_name', age:'_age' } }
 *	@desc A map where keys=public names and values=name of private attribute.
 *
 *	@example { getset: { name:[getter,setter], age:[getter,setter] } }
 *	@desc A map where keys=public names and values=array of 2 functions that handle the getting/setting.
 */

Class.extensions( 'getset', function( data, list ){
	if( list.split )
		list = list.split(/[,\s]+/);
		
	if( list.constructor == Array )
		for( var i = list.length; i--; )
			create( list[i] );
	else
		for( var key in list )
			create( key, list[key] );
	
	function create( prop, member ){
		member = member || prop;
		var Prop = prop.charAt(0).toUpperCase() + prop.slice(1);
		data.m['get'+Prop] = function(){
			if( member.splice )
				return member[0].call( this, prop );
			return this[member];
		};
		data.m['set'+Prop] = function( value ){
			if( member.splice )
				return member[1].call( this, value, prop );
			return this[member] = value;
		};
	};
});

/** 
 * Enables you to create interfaces that define methods and have classes that do implement them.
 * @author Ariel Flesler
 */

/**
 * @contructor
 * @param {String, Array} methods List of method names that need to be implemented (strings are splitted by comma or whitespace)
 */
var Interface = Class(function( methods ){
	this.methods = methods.split ? methods.split(/[,\s]+/) : methods;
	this.uid = ++this.clazz._uid_;
});

Interface.statics('_uid_', 0);

/**
 * @method
 * @memberOf Interface
 * @param {Class, Instance} obj Class or instance that needs to implement this interface.
 * @returns true if all the methods are implemented or the name of the first non-implemented method.
 */
Interface.members( 'isImplementedBy', function( obj ){
	var i = this.methods.length;
	obj = obj.prototype || obj;// Class or instance
	
	if( !obj.implemented[this.uid] ){	
		while( i-- )
			if( this.methods[i] in obj == false )
				return this.methods[i];
		
		obj.implemented[this.uid] = true;
	}	
	return true;
});

// ifaces can be one interface object or an array of objects
Class.extensions( 'implement', function( data, ifaces ){
	data.m.ifaces = ifaces.concat ? ifaces : [ ifaces ];
	
	var old = data.c, ok = false;
	data.c = function(){
		if( !ok ){
			var method, i = 0;
			while( this.ifaces[i] )
				if( (method = this.implement(this.ifaces[i++]) ) !== true )
					throw new Error('A class failed to implement the method "'+method+'".');
			ok = true;
		}
		old.apply(this,arguments);
	};
});

/**
 * @id SomeClass.implement
 * @id someInstance.implement
 * @memberOf Class
 * @param {Instance} iface Interface instance to check for implementation against the calling class.
 * @returns true if all the methods are implemented or the name of the first non-implemented method.
 */
Class.staticsList.implement = Class.prototype.implement = function( iface ){
	return iface.isImplementedBy( this );
};

Class.after.push(function( data ){
	data.m.implemented = {};
});

/** 
 * Saves the created class into the specified module. It also creates the namespace for you.
 * @author Ariel Flesler
 */

/**
 * Ways to specify the module:
 *	@example { module: 'my.best.module' }
 *	@desc A dot separated namespace.
 *
 *	@example { module: ['my', 'best', 'module'] }
 *	@desc An array of nested names.
 *
 *	@example { module: [window.my, 'best.module'] }
 *	@desc A dot separated namespace relative to an actual object/module.
 *
 *	@example { module: [window.my, 'best', 'module'] }
 *	@desc An array of names relative to an object/module.
 */
Class.extensions( 'module', function( data, names ){
	var scope = window;
	
	if( names.split )
		names = names.split('.');
	else if( typeof names[0] != 'string' ){
		scope = names.shift();
		if( names[0].split )
			names = names[0].split('.');
	}
	
	// the names including the namespace
	data.s.fullName = names.join('.');
	// only the actual class name
	data.s.className = names.pop();
	// the whole namespace w/o class name
	data.s.moduleName = names.join('.');
	
	while( names.length ){
		var name = names.shift();
		scope = scope[name] = scope[name] || {};
	}
	// the module object
	data.s.module = scope;
	
	data.a.push(function(){
		scope[data.s.className] = data.c;
	});
});

/** 
 * Handy shortcut to create properties.
 * @author Ariel Flesler
 */

/**
 * Ways to specify the list of properties:
 *	@example { properties: 'name, age, gender' }
 *	@desc A comma separated list of names. The private attribute needs to have that name.
 *
 *	@example { properties: ['name', 'age', 'gender'] }
 *	@desc An array of names. The private attribute needs to have that name.
 *
 *	@example { properties: { name:'_name', age:'_age' } }
 *	@desc A map where keys=public names and values=name of private attribute.
 *
 *	@example { properties: { name:fnName, age: fnAge } }
 *	@desc A map where keys=public names and values=function that handles the setting/getting.
 */

Class.extensions( 'properties', function( data, list ){
	if( list.split )
		list = list.split(/, ?/);
		
	if( list.constructor == Array )
		for( var i = list.length; i--; )
			create( list[i] );
	else
		for( var key in list )
			create( key, list[key] );
	
	function create( prop, member ){
		member = member || prop;
		var Prop = prop.charAt(0).toUpperCase() + prop.slice(1);
		data.m[Prop] = function( value ){
			if( typeof member == 'function' )
				return member.call( this, value, prop );
			if( arguments.length )
				this[member] = value;
			return this[member];
		};
	};
});

/** 
 * Enables the creation of singleton classes. This can only generate one instance.
 * @author Ariel Flesler
 */

// name is the static method of the class to retrieve the instance
// if just true is passed, the default is used: getInstance.
Class.extensions( 'singleton', function( data, name ){
	if( !name )
		return;
	if( name == true )
		name = 'getInstance';
		
	var instance, 
		old = data.c;
	
	data.c = function(){
		if( !instance ){
			if( !(this instanceof data.c) )
				return new data.c;
			instance = this;
			old.call(this);
		}
		return instance;
	};
	data.s[name] = function(){
		return this();
	};
});

/** 
 * Define statics and members on class creation, instead of calling statics() or members() afterwards.
 * @author Ariel Flesler
 */

// list must be a map, it's not possible to pass one key and value
Class.extensions({
	statics: function( data, list ){
		Class.copy( data.s, list );
	},
	members: function( data, list ){
		Class.copy( data.m, list );
	}
});

/** 
 * Adds pretty dump for classes and instances. This is mostly a demo of how to do that.
 * @author Ariel Flesler
 */

Class.toString = Class.staticsList.toString = function(){
	return '[Class]';
};

Class.members('toString', function(){
	return '[Instance]';
});

Class.after.push(function( data ){
	if( data.c.toString == Function.prototype.toString )
		data.c.toString = Class.staticsList.toString;
});
