/*
fSignatures 0.1

MIT License

Copyright (c) 2008 Francois Savard (http://www.fsavard.com)

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/

(function(context){	
	///////////////////////////////////////////////////////
	// TypeChecker and associated machinery
	
	var TypeChecker = function(tcCompareString, checkerFunction){
		// Throws an error if doesn't match
		this.check = checkerFunction;
		
		// Useful to compare function signatures
		this.tcCompareString = tcCompareString;
	}
	
	var checkType = function(typeChecker, actualVar){
		// If nothing was specified, we take it checking is optional
		if(!typeChecker){
			return actualVar;
		}
		
		typeChecker.check(actualVar);
		
		return actualVar;
	}

	var checkMultipleTypes = function(typeCheckers, repeatLastArg, actualVars){
		var newArgs = [];
		
		// If nothing was specified, we take it checking is optional
		if(!typeCheckers){
			return actualVars;
		}
		
		if(!repeatLastArg && typeCheckers.length < actualVars.length){
			throw new Error("Too many arguments, expected "+typeCheckers.length+", got "+actualVars.length);
		}
		
		var i=0;
		var max=Math.min(typeCheckers.length, actualVars.length);
		for(; i<max; i++){
			newArgs.push(typeCheckers[i].check(actualVars[i]));
		}
		
		// Actual arguments and types might differ in length since some
		// arguments may be optional, so we call the TypeCheckers with
		// an undefined value to see if they are truly optional.
		for(; i<typeCheckers.length; i++){
			newArgs.push(typeCheckers[i].check(undefined));
		}
		
		// At last, if we had more arguments than typecheckers and the
		// last argument may be repeated, we check the remaining arguments
		// against the last typechecker.
		if(repeatLastArg && typeCheckers.length < actualVars.length){
			for(; i<actualVars.length; i++){
				newArgs.push(typeCheckers[typeCheckers.length-1].check(actualVars[i]));
			}
		}
		
		return newArgs;
	}
	
	///////////////////////////////////////////////////////
	// Basic TypeCheckers
	
	var BasicTCs = {};
	
	BasicTCs.opt = BasicTCs._opt = function(innerTypeChecker, defaultValue){
		return new TypeChecker(
			"Optional "+innerTypeChecker.tcCompareString,
			function(val){
				if(val === undefined || val === null){
					return defaultValue;
				}
				
				return innerTypeChecker.check(val);
			}
		)
	};
	
	BasicTCs.typeofChecker = function(typeString){
		return new TypeChecker(
			"typeof " + typeString,
			function(val){
				if(typeof val != typeString){
					throw new Error("Expected typeof="+typeString+", got typeof="+typeof val);
				}
				
				return val;
			}
		)
	};
	
	///////////////
	// Simple typecheckers based on typeof
	
	BasicTCs._str = BasicTCs._string =
		BasicTCs['string'] = BasicTCs['str'] =
		BasicTCs.typeofChecker('string');
	
	BasicTCs._bool = BasicTCs._boolean =
		BasicTCs['bool'] = BasicTCs['boolean'] =
		BasicTCs.typeofChecker('boolean');
	
	BasicTCs._undef = BasicTCs._undefined =
		BasicTCs['undef'] = BasicTCs['undefined'] =
		BasicTCs.typeofChecker('undefined');
	
	BasicTCs._num = BasicTCs._number =
		BasicTCs['num'] = BasicTCs['number'] =
		BasicTCs.typeofChecker('number');
	
	// Doing isFunction() correctly seems quite complicated,
	// so I took an existing solution and the test cases that go along with it.
	// Taken from http://dhtmlkitchen.com/jstest/isFunctionTest.html
	// Other possibility at
	// http://api.dojotoolkit.org/jsdoc/dojo/HEAD/dojo.isFunction
	BasicTCs._func = BasicTCs._function =
		BasicTCs['func'] = BasicTCs['function'] = new TypeChecker(
		"function",
		function(val){
			if(typeof val != "function")
				throw new Error("Expected a function, got typeof="+typeof val);
			if(typeof val.constructor != "function")
				throw new Error("Expected a function, but constructor is not function");
			if(!val.constructor.prototype.hasOwnProperty( "call" ))
				throw new Error("Expected a function, but constructor.prototype doesn't have call property.");
			
			return val;
		}
	);
	
	// Typeof didn't work very well here, for some reason
	BasicTCs._null = BasicTCs['null'] = new TypeChecker(
		"null",
		function(val){
			if(val === null){
				return val;
			}
			
			throw new Error("Expected explicitely 'null' type, got "+typeof val);
		}
	);
	
	BasicTCs._array = BasicTCs['array'] = new TypeChecker(
		"arrayLike",
		function(val){
			if(val && val.constructor == Array){
				return val;
			}
			
			throw new Error("Expected an array, got "+typeof val);
			
			/*
			This worked well except in MSIE 6, throwing an error.
			
			// Source: http://bytes.com/forum/thread577118.html
			// Reply by Douglas Crockford
			if(val
				&& typeof val === 'object'
				&& typeof val.length === 'number'
				&& !(val.propertyIsEnumerable('length'))){
				return val;
			}
			
			throw new Error("Expected an array-like object, got "+typeof val);
			*/
		}
	);
	
	BasicTCs._typedArray = function(elementTC){
		return new TypeChecker(
			"typedArray "+elementTC.tcCompareString,
			function(val){
				BasicTCs._array.check(val);
				
				for(var i=0; i<val.length; i++){
					elementTC.check(val[i]);
				}
				
				return val;
			}
		)
	}
	
	///////////////////////////////////////////////////////
	// Signature chaining (args(), ret()) and func() wrapper
	
	var Signature = function(){
		this.argTypes = null;
		this.returnType = null;
		this.mayRepeatLastArg = false;
	}
	
	Signature.prototype.args = function(){
		for(var i=0; i<arguments.length; i++){
			if(typeof arguments[i] == 'string'){
				if(!context.ty[arguments[i]]){
					throw Error('String-specified type not found: '+arguments[i]);
				}
				
				arguments[i] = context.ty[arguments[i]];
			}
		}
		
		this.argTypes = arguments;
		
		return this;
	}
	
	Signature.args = function(){
		var obj = new Signature();
		return obj.args.apply(obj, arguments);
	}
	
	Signature.prototype.repeatLastArg = function(){
		this.mayRepeatLastArg = true;
		
		return this;
	}
	
	Signature.prototype.returns = function(returnType){
		if(typeof returnType == 'string'){
			if(!context.ty[returnType]){
				throw Error('String-specified type not found: '+returnType);
			}
			
			returnType = context.ty[returnType];
		}
		
		if(!returnType){
			// undefined means nothing specified, null that we
			// must not have a return value
			this.returnType = null;
		}else{
			this.returnType = returnType;
		}
		
		return this;
	}
	
	Signature.returns = function(returnType){
		var obj = new Signature();
		return obj.returns.apply(obj, arguments);
	}
	
	Signature.prototype.equals = function(otherSignature){
		if(this.mayRepeatLastArg != otherSignature.mayRepeatLastArg){
			return false;
		}
		
		if(this.returnType.tcCompareString != otherSignature.returnType.tcCompareString){
			return false;
		}
		
		if(this.argTypes.length != otherSignature.argTypes.length){
			return false;
		}
		
		for(var i=0; i<this.argTypes.length; i++){
			if(this.argTypes[i].tcCompareString != otherSignature.argTypes[i].tcCompareString){
				return false;
			}
		}
		
		return true;
	}
	
	Signature.prototype.f = function(wrappedFunction){
		var argTypes = this.argTypes;
		var retType = this.returnType;
		var repeatLastArg = this.mayRepeatLastArg;
		
		var retFunc = function(){
			var newArgs = checkMultipleTypes(argTypes, repeatLastArg, arguments);
			var retVal = wrappedFunction.apply(this, newArgs);
			checkType(retType, retVal);
			return retVal;
		}
		
		// Remembering the signature explicitly is the foundation
		// needed for interface checking
		retFunc._signature = this;
		
		return retFunc;
	}
	
	///////////////////////////////////////////////////////
	// Interface checking
	
	// Members: a JS map of functions names to signatures
	var InterfaceChecker = function(members){
		this.members = members;
	}
	InterfaceChecker.prototype = TypeChecker;
	
	var constructInterfaceChecker = function(members){
		return new InterfaceChecker(members);
	}
	
	InterfaceChecker.prototype.check = function(actualObject){
		for(var functionName in this.members){
			if(!(actualObject[functionName] instanceof Function)){
				throw new Error("Interface method not found in object: "+functionName);
			}
			
			if(!(actualObject[functionName]._signature)){
				throw new Error("Attempting to check method "+functionName+", which has no explicit signature.");
			}
			
			if(!this.members[functionName].equals(actualObject[functionName]._signature)){
				throw new Error("Method signature for "+functionName+" does not match interface specification.");
			}
		}
		
		return actualObject;
	}
	
	///////////////////////////////////////////////////////
	// Add shortcuts to context
	
	context.ty = BasicTCs;
	
	// Core syntax helpers
	context.tc = {};
	context.tc.args = Signature.args;
	context.tc.returns = Signature.returns;
	context.tc.interfaceChecker = constructInterfaceChecker;
	
	// exposing private functions for tests
	context.tc._TypeChecker = TypeChecker;
	context.tc._Signature = Signature;
	context.tc._checkType = checkType;
	context.tc._checkMultipleTypes = checkMultipleTypes;
})($);

