/*
 * Validator.js - Version 0.1
 *
 * $Id: Validator.js 10 2009-05-16 13:49:22Z terence $
 *
 * Copyright (c) 2008 Terence Honles (terence.honles.com)
 * Dual licensed under the MIT (MIT-LICENSE.txt)
 * and GPL (GPL-LICENSE.txt) licenses.
 *
 */

function $Validator(localTo) {
// munging constants
var undefined;
var Infinity=Infinity;

// The Error that should be used when an assertion
// is asserted and does not exist in the Assert collection
function AssertionNotSupportedError(assertion) {
	this.name = "AssertionNotSupported";
	this.assertion = assertion;
	this.message = assertion.name;
	this.error = new Error();
	return this;
}

// Helper method to print an AssertionNotSupportedError
AssertionNotSupportedError.prototype.toString = function() {
	return this.name + ": " + this.message;
}

// The Error that should be used when an assertion fails
// 'assertion' should be the assertion which failed
// 'message' is an optional parameter which may detail why the assertion failed
function AssertionFailedError(assertion, message) {
	this.name = "AssertionFailedError";
	this.assertion = assertion;
	this.message = assertion.name;
	if (message) this.message += " " + message;
	this.error = new Error();
	return this;
}

// Helper method to print an AssertionFailedError
AssertionFailedError.prototype.toString = function() {
	return this.name + ": " + this.message;
}

// The Assertions Available
//
// to extend add another {key, value} pair
// the key should be the name of the assertion
// the value should consist of:
//   - the name of the assertion (case sensitive)
//   - the function to assert
//   - and an optional list of parameters for the assertion function
var Assert = {
	// Asserts that a value exists
	Exists:{
		name:"Exists",
		assert: function(value) {
			if (value == null || value == undefined) throw new AssertionFailedError(Assert.Exists);
		}
	},

	// Asserts that a value exists and is not empty
	NotEmpty:{
		name:"NotEmpty",
		assert: function(value) {
			if (value.length == 0) throw new AssertionFailedError(Assert.NotEmpty);
		}
	},
	
	// Asserts that a value is a valid email address
	IsEmail:{
		name:"IsEmail",
		// A valid email address is given by the following regular expression
		// should match: (not a complete list)
		//   - name@domain.com
		//   - first.last@domain.com
		//   - first.last@sub.domain.com
		//   - last87@domain.com
		parameters:{re:new RegExp("^(?:[A-Z0-9_+-]+[.]?|%[A-Z0-9]{2}[.]?)*(?:[A-Z0-9_+-]+|%[A-Z0-9]{2})@(?:[A-Z0-9-]+[.])+[A-Z0-9-]+$", "i")},
		assert: function(value) {
			Assert.IsString.assert(value);
			Assert.NotEmpty.assert(value);
			if (!Assert.IsEmail.parameters.re.test(value)) throw new AssertionFailedError(Assert.IsEmail, "('" + value + "')");
		}
	},

	// Asserts that a value is not empty and is also a number
	IsNumber:{
		name:"IsNumber",
		assert: function(value) {
			Assert.Exists.assert(value);
			var num = Number(value);
			if (isNaN(num)) throw new AssertionFailedError(Assert.IsNumber, "('" + value + "')");
			return num;
		}
	},
	
	IsPhone:{
		name:"IsPhone",
		// A valid phone number is given by the following regular expression
		// should match: (where '[item]' is optional)
		//   - [1] 213 [-] 123 [-] 4567
		//   - [1] (213) [-] 123 [-] 4567
		//   - [1]2131234567
		//   - 1.213.123.1234
		parameters:{re:new RegExp("^\\+?1?[-.]?(?:\\((\\d{3})\\)|(\\d{3}))[-.]?(\\d{3})[-.]?(\\d{4})$")},
		assert: function(value) {
			var value = value.replace(/\s/g, '');
			Assert.IsString.assert(value);
			Assert.NotEmpty.assert(value);
			
			var phoneNumber = Assert.IsPhone.parameters.re.exec(value);
			if (phoneNumber == null) throw new AssertionFailedError(Assert.IsPhone, "('" + value + "')");
			
			return (phoneNumber[1]?phoneNumber[1]:phoneNumber[2]) + phoneNumber[3] + phoneNumber[4];
		}
	},

	// Asserts that a value is either a string primitive or an instance of the string class
	IsString:{
		name:"IsString",
		assert: function(value) {
			Assert.Exists.assert(value);
			if (!(value instanceof String) && typeof value != "string")
				throw new AssertionFailedError(Assert.IsString);
		}
	},
	
	// Asserts that a string begins with a specified substring
	BeginsWith:{
		name:"BeginsWith",
		parameters:{re:null},
		assert: function(value) {
			Assert.IsString.assert(value);
			Assert.Exists.assert(Assert.BeginsWith.parameters.re);
			if (value.search(Assert.BeginsWith.parameters.re) != 0) throw new AssertionFailedError(Assert.BeginsWith, "('" + value + "')");
		}
	},
	
	// Helper method which sets the substring to match
	beginsWith: function(re) {
		if (!re) return Assert.BeginsWith;
	
		if (re instanceof String || typeof re == "string") {
			var re = new RegExp(re);
		}
		
		Assert.BeginsWith.parameters.re = re;
		return Assert.BeginsWith;
	},
	
	// Asserts that a string contains a specified substring
	Contains:{
		name:"Contains",
		parameters:{re:null},
		assert: function(value) {
			Assert.IsString.assert(value);
			Assert.Exists.assert(Assert.Contains.parameters.re);
			if (value.search(Assert.Contains.parameters.re) == -1) throw new AssertionFailedError(Assert.Contains, "('" + value + "')");
		}
	},
	
	// Helper method which sets the substring to match
	contains: function(re) {
		if (!re) return Assert.Contains;
	
		if (re instanceof String || typeof re == "string") {
			var re = new RegExp(re);
		}
		
		Assert.Contains.parameters.re = re;
		return Assert.Contains;
	},
	
	// Asserts that a string contains a specified substring
	DoesntContain:{
		name:"DoesntContain",
		parameters:{re:null},
		assert: function(value) {
			Assert.IsString.assert(value);
			Assert.Exists.assert(Assert.DoesntContain.parameters.re);
			if (value.search(Assert.DoesntContain.parameters.re) != -1) throw new AssertionFailedError(Assert.DoesntContain, "('" + value + "')");
		}
	},
	
	// Helper method which sets the substring to match
	doesntContain: function(re) {
		if (!re) return Assert.DoesntContain;
	
		if (re instanceof String || typeof re == "string") {
			var re = new RegExp(re);
		}
		
		Assert.DoesntContain.parameters.re = re;
		return Assert.DoesntContain;
	},
	
	// Asserts that a string ends with a specified substring
	EndsWith:{
		name:"EndsWith",
		parameters:{re:null},
		assert: function(value) {
			Assert.IsString.assert(value);
			Assert.Exists.assert(Assert.EndsWith.parameters.re);
			var index = value.search(Assert.EndsWith.parameters.re);
			// when converting to a string the constructor saves the '/'
			var asString = new String(Assert.EndsWith.parameters.re);
			if (index + asString.length - 2 != value.length) throw new AssertionFailedError(Assert.EndsWith, "('" + value + "')");
		}
	},

	// Helper method which sets the substring to match
	endsWith: function(re) {
		if (!re) return Assert.EndsWith;
		
		if (re instanceof String || typeof re == "string") {
			var re = new RegExp(re);
		}
		
		Assert.EndsWith.parameters.re = re;
		return Assert.EndsWith;
	},
	
	// Asserts that a string has at least the minimum length
	MinLength:{
		name:"MinLength",
		parameters:{minlen:6},
		assert: function(value) {
			Assert.IsString.assert(value);
			if (value.length < Assert.MinLength.parameters.minlen) throw new AssertionFailedError(Assert.MinLength, "('" + value + "')");
		}
	},
	
	// Helper method which sets the min length parameter and returns
	// Assert.MinLength (which can be used in a assert statement)
	// (ie. <Validator>.assert(minLength(10)) -- asserts a min length of 10)
	minLength: function(len) {
		if (len == null || len == undefined) return Assert.MinLength;
	
		Assert.MinLength.parameters.minlen = len;
		return Assert.MinLength;
	},
	
	// Asserts that a string has at most the maximum length
	MaxLength:{
		name:"MaxLength",
		parameters:{maxlen:100},
		assert: function(value) {
			Assert.IsString.assert(value);
			if (value.length > Assert.MaxLength.parameters.maxlen) throw new AssertionFailedError(Assert.MaxLength, "('" + value + "')");
		}
	},
	
	// Helper method which sets the max length parameter and returns
	// Assert.MaxLength (which can be used in a assert statement)
	// (ie. <Validator>.assert(maxLength(10)) -- asserts a max length of 10)
	maxLength: function(len) {
		if (len == null || len == undefined) return Assert.MaxLength;
	
		Assert.MaxLength.parameters.maxlen = len;
		return Assert.MaxLength;
	},
	
	// Asserts that a string does not have a sequence of the specified length
	NoSequence:{
		name: "NoSequence",
		// Number of characters to flag as a sequence
		parameters:{len:3},
		assert: function(value) {
			Assert.IsString.assert(value);
			
			var seqlen = Assert.NoSequence.parameters.len;
			
			if (seqlen > 0) {
				var max = value.length - seqlen + 1;
				for (var i=0; i<max; i++) {
					var charCode = value.charCodeAt(i);
					var nextCode = value.charCodeAt(i+1);
					var difference = charCode-nextCode;
					
					if (difference == 1 || difference == -1) {
						var match = true;
						
						for (var j=1; j<seqlen-1; j++) {
							charCode = nextCode;
							nextCode = value.charCodeAt(i+j+1);
							
							if (difference != charCode-nextCode) {
								match = false;
								break;
							}
						}
						
						if (match) throw new AssertionFailedError(Assert.NoSequence, "('" + value + "')");
					}
				}
			}
		}
	},
	
	// Helper method which sets no sequences parameters
	noSequence: function(len) {
		if (len != null && len != undefined) {
			Assert.NoSequence.parameters.len = len;
		}
		
		return Assert.NoSequence;
	},
	
	// Asserts that a string contains an alpha charater
	ContainsAlpha:{
		name: "ContainsAlpha",
		assert: function(value) {
			Assert.IsString.assert(value);
			
			if (!/[a-zA-Z]/.test(value)) throw new AssertionFailedError(Assert.ContainsAlpha, "('" + value + "')");
		}
	},
	
	// Asserts that a string contains a numeric charater
	ContainsNumeric:{
		name: "ContainsNumeric",
		assert: function(value) {
			Assert.IsString.assert(value);
			
			if (!/\d/.test(value)) throw new AssertionFailedError(Assert.ContainsNumeric, "('" + value + "')");
		}
	},
	
	// Asserts that a string contains an uppercase charater
	ContainsUpper:{
		name: "ContainsUpper",
		assert: function(value) {
			Assert.IsString.assert(value);
			
			if (!/[A-Z]/.test(value)) throw new AssertionFailedError(Assert.ContainsUpper, "('" + value + "')");
		}
	},
	
	// Asserts that a string contains a lowercase charater
	ContainsLower:{
		name: "ContainsLower",
		assert: function(value) {
			Assert.IsString.assert(value);
			
			if (!/[a-z]/.test(value)) throw new AssertionFailedError(Assert.ContainsLower, "('" + value + "')");
		}
	},
	
	// Asserts that a string contains a 
	ContainsSymbol:{
		name: "ContainsSymbol",
		assert: function(value) {
			Assert.IsString.assert(value);
			
			// if it's not an alpha and not a number or whitespace then we're assuming it's a symbol
			if (!/[^a-zA-Z0-9 \f\n\r\t\v\u00A0\u2028\u2029]/.test(value)) throw new AssertionFailedError(Assert.ContainsSymbol, "('" + value + "')");
		}
	},

	// Asserts that a string meets the specified complexity
	// (complexity is controlled by the parameters below)
	Complexity:{
		name:"Complexity",
		parameters:{min:6, sequence:true, seqlen:3, alpha:true, numeric:false, lower:false, upper:false, symbol:false},
		assert: function(value) {
			var param = Assert.Complexity.parameters;
		
			Assert.IsString.assert(value);
			Assert.minLength(param.min).assert(value);
			
			if (param.sequence) Assert.noSequence(param.seqlen).assert(value);
			if (param.alpha) Assert.ContainsAlpha.assert(value);
			if (param.numeric) Assert.ContainsNumeric.assert(value);
			if (param.lower) Assert.ContainsLower.assert(value);
			if (param.upper) Assert.ContainsUpper.assert(value);
			if (param.symbol) Assert.ContainsSymbol.assert(value);
		}
	},
	
	// Helper method which sets complexity parameters
	complexity: function(minlen, sequence, seqlen, alpha, numeric, lower, upper, symbol) {
		var param = Assert.Complexity.parameters;
	
		if (minlen != null && minlen != undefined) param.min = minlen;
		if (sequence != null && sequence != undefined) 	param.sequence = sequence;
		if (seqlen != null && seqlen != undefined) param.seqlen = seqlen;
		if (alpha != null && alpha != undefined) param.alpha = alpha;
		if (numeric != null && alpha != undefined) param.numeric = numeric;
		if (lower != null && lower != undefined) param.lower = lower;
		if (upper != null && upper != undefined) param.upper = upper;
		if (symbol != null && symbol != undefined) param.symbol = symbol;
		
		return Assert.Complexity;
	}
}

// The Validator constructor
// if the number of arguments are:
//    0  - creates an empty Validator
//    1  - creates a Validator initialized with the parameter given
//         which is identical to calling (new Validator).push(parameter);
//    2+ - creates an Array of Validators initialized with the parameters given
function Validator() {
	if (arguments.length == 0) return;

	if (arguments.length == 1) {
		this.analyze = arguments[0];
	} else {
		var num = arguments.length;
		var arr = new Array();
		for (var i=0; i<num; i++) {
			arr[i] = new Validator(arguments[i]);
		}

		return arr;
	}
}

// Sets the object to assert
// returns: itself (for chaining)
Validator.prototype.push = function(value) {
	this.analyze = value;
	return this;
}

// Sets a parameter in the assertions
// 'assertion' should either be a string or assertion
// 'key' is the parameter to set on the assertion
// 'value' is the value of the parameter to set on the assertion
// returns: itself (for chaining)
//
// If there are only two arguments then key is interpreted as
// an object from what the assertion parameters is supposed to mirror
//
Validator.prototype.set = function(assertion, key, value) {
	if (arguments.length == 2) {
		assertion = this.checkAssertion(assertion);
		
		if (assertion.parameters == null || assertion.parameters == undefined)
			throw new Error("Assertion '" + assertion.name + "' does not expect parameters")
	
		for (var i in key)		
			assertion.parameters[i] = key[i];
		
	} else if (arguments.length == 3) {
		assertion = this.checkAssertion(assertion);

		if (assertion.parameters == null || assertion.parameters == undefined)
			throw new Error("Assertion '" + assertion.name + "' does not expect parameters")

		assertion.parameters[key] = value;
	}

	return this;
}

// Sets a parameter in the assertions
// 'assertion' should either be a string or assertion
// 'key' is the parameter to set on the assertion
// 'value' is the value of the parameter to set on the assertion
//
// If there are only two arguments then key is interpreted as
// an object from what the assertion parameters is supposed to mirror
//
// This has the same functionality as 'set' but it warns
// the user when they are creating instead of updating a parameter
// returns: itself (for chaining)
Validator.prototype.update = function(assertion, key, value) {
	if (arguments.length == 2) {
		assertion = this.checkAssertion(assertion);
		
		if (assertion.parameters == null || assertion.parameters == undefined)
			throw new Error("Assertion '" + assertion.name + "' does not expect parameters")

		for (var i in key) {
			if (!(i in assertion.parameters))
				throw new Error("Assertion '" + assertion.name + "' does not expect the parameter: " + i);
				
			assertion.parameters[i] = key[i];
		}
	} else if (arguments.length == 3) {
		assertion = this.checkAssertion(assertion);

		if (assertion.parameters == null || assertion.parameters == undefined)
			throw new Error("Assertion '" + assertion.name + "' does not expect parameters")

		if (!(key in assertion.parameters))
			throw new Error("Assertion '" + assertion.name + "' does not expect the parameter: " + key);

		assertion.parameters[key] = value;
	}

	return this;
}

// Makes sure that an assertion exists in the available assertions
// if the parameter is a string it fetches the assertion named
// and returns it
Validator.prototype.checkAssertion = function(assertion) {
	if (assertion instanceof String || typeof assertion == "string") {
		if (assertion in Assert) var assertion = Assert[assertion];
		else throw new AssertionNotSupportedError(assertion);
	} else if (!(assertion.name in Assert)) throw new AssertionNotSupportedError(assertion);
	
	if (typeof assertion == "function") return assertion();
	return assertion;
}

// Asserts an assertion on a value
// If the number of arguments are:
//    1  - Asserts the assertion on the saved value.
//         if the assertion returns a value it is saved for
//         later reference
//    2+ - Asserts the assertion on the following parameters
// returns: itself (for chaining)
Validator.prototype.assert = function(assertion) {
	if (assertion == null || assertion == undefined) return this;

	assertion = this.checkAssertion(assertion);
	if (arguments.length > 1) {
		var num = arguments.length;
		for (var i=1; i<num; i++) assertion.assert(arguments[i]);
	} else {
		var result = assertion.assert(this.analyze);
		if (result != null && result != undefined) this.result = result;
	}

	return this;
}

Validator.prototype.pop = function() {
	return this.result;
}


// exposing function
function exposeValidate(localTo) {
	localTo.Validator = Validator;
	localTo.AssertionNotSupportedError = AssertionNotSupportedError;
	localTo.AssertionFailedError = AssertionFailedError;
	localTo.Assert = Assert;
	localTo.exposeValidate = exposeValidate;
}

exposeValidate(localTo);
}

$Validator(window);
