/**
 * ClientValidator Base Class
 *
 * Expected to be extended by DK,SE,NO etc variants
 */
function CS_ClientValidator_Base(){}

/**
 * Allow inheritance of CS_ClientValidator_Base
 */
CS_ClientValidator_Base.extend = function(childc)
{
	var superc = CS_ClientValidator_Base;
	childc.__parent = superc;
	for (var property in superc.prototype) {
		if (typeof childc.prototype[property] == "undefined") {
			childc.prototype[property] = superc.prototype[property];
		}
	}
	return childc;
}

CS_ClientValidator_Base.prototype = 
{
	/**
	 * Max number of chains (just to prevent major hangs)
	 * @private
	 */
	_chain_depth_limit : 10,
	_chain_depth_count : 0,
	
	/**
	 * Form object subject to validation
	 * @private
	 */
	_data : null,
	
	/**
	 * Validators that will be applied to form
	 * @private
	 */
	_validators : {},
	
	/**
	 * Validators that errored out during isValid execution
	 * @private
	 */
	_errors : [],
	
	/**
	 * First error message of each field found in _errors
	 * @private
	 */
	_display_errors : {},
	
	/**
	 * Fields that should be optionally left empty
	 * @private
	 */
	_optional_fields : [],
	
	setForm : function(form)
	{
		this._data = form;
	},
	
	setValidatorsArray : function(validators_array)
	{
		var chain = this._prepareValidatorsArrayChain(validators_array);
		this._mergeChains(this._validators, chain);
	},
	
	addValidator : function(field_name, method, error_msg, args)
	{
		this._addValidator(field_name, method, error_msg, args);
	},
	
	setOptionalFields : function(fields)
	{
		if(typeof fields != 'object')
		{
			throw new Error('First argument "fields" of setOptionalFields must be an array');
		}
		
		this._optional_fields = fields;
	},
	
	/**
	 * Run validators against form object
	 * @param object form (optional)
	 * @type boolean
	 */
	isValid : function(form)
    {
    	this._reset();
    	
    	if(typeof form == 'object')
    	{
    		this.setForm(form);
    	}
    	
    	this.onValidateBegin();
    	
    	try
    	{
    		// validate main chain
    		this._validateChain(this._validators);
    	}
    	catch(ex)
    	{
    		if(ex.name == 'CS_ClientValidator_Exception') {
	    		alert('cs: ' + ex.message);
	    		return false;
    		}
    		throw ex;
    	}
    	
    	if(this._errors.length == 0)
    	{
    		// only return false if explicitly returned by event
    		return (this.onValidateSuccess() === false) ? false : true;
    	}
    	else
    	{
    		// only return true if explicitly returned by event
    		return (this.onValidateFailed() === true) ? true : false;
    	}
    },
    
    /**
     * Get value of field by name (supports regular input, radio and select)
     * @param {String} field_name
     * @type String
     * @return string|false
     */
	getFieldValue : function(field_name)
    {
    	if(typeof this._data[field_name] == 'undefined')
    	{
    		throw this._newException('field "' + field_name + '" is undefined');
    	}
    	
    	var obj = this._data[field_name];
    	
    	if(obj.type == 'select-one')
    	{
    		return obj.options[obj.selectedIndex].value;
    	}
    	else if(obj.type == 'checkbox')
    	{
    		if(obj.checked)
    		{
    			return obj.value;
    		}
    		return null;
    	}
    	else if(typeof obj[0] != 'undefined' && typeof obj[0].type != 'undefined' && obj[0].type == 'radio')
    	{
    		for(var i = 0; i < obj.length; i++)
    		{
    			if(obj[i].checked)
    			{
    				return obj[i].value;
    			}
    		}
    		return null; 
    	}
    	else if(typeof obj.value != 'undefined')
    	{
    		return obj.value;
    	}
    	else
    	{
			if((ret = this.onGetFieldValueError(field_name)))
			{
				return ret;
			}
			return false;
    	}
    },
    
	/**
	 * Validation is about to begin
	 */
	onValidateBegin : function()
	{
		
	},
	
	/**
	 * Validation succeded event
	 * @return true (optional) value passed on as return value for isValid
	 */
	onValidateSuccess : function()
	{
		
	},
	
	/**
	 * Validation fail event
	 * @return false (optional) value passed on as return value for isValid
	 */
	onValidateFailed : function()
	{
		
	},
	
	/**
	 * Error getting value of field event
	 */
	onGetFieldValueError : function(field_name)
	{
		throw this._newException('failed to get value of "' + field_name + '" unknown field type');
	},
	
	validNotEmpty : function(field_name)
    {
    	var value = this.getFieldValue(field_name);
    	return value != null && value != '';
    },
	
    validFirstname : function(field_name)
    {
    	var ret = this._data[field_name].value.length > 1;
    	return ret;
    },
    
    validLastname : function(field_name)
    {
    	var ret = this._data[field_name].value.length > 1;
    	return ret;
    },
    
    validFullname : function(field_name)
    {
    	var expr = /^\S{2,} \S{2,}.*$/i;
    	var ret = expr.exec(this._data[field_name].value);
    	return !(ret == null);
    },
    
    validEmail : function(field_name)
    {
    	var expr = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
    	var ret = expr.exec(this._data[field_name].value);
    	return !(ret == null);
    },
    
    validAddress : function(field_name)
    {
    	var ret = this._data[field_name].value.length > 1;
    	return ret;
    },
    
    validStreetname : function(field_name)
    {
    	var ret = this._data[field_name].value.length > 1;
    	return ret;
    },
    
    validStreetNumber : function(field_name)
    {
    	return this.validNotEmpty(field_name);
    },
    
    validPostcode : function(field_name)
    {
    	throw this._newException('CS_ClientValidator_Base.validPostcode is not implemented');
    },
    
    validCity : function(field_name)
    {
    	var ret = this._data[field_name].value.length > 1;
    	return ret;
    },
    
    validGender : function(field_name)
    {
    	var ret = this.validWhiteList(field_name, {options: ["male","female"]});
    	return ret;
    },
    
    validPhone : function(field_name)
    {
    	throw this._newException('CS_ClientValidator_Base.validPhone is not implemented');
    },
    
    validBirthdateYear : function(field_name)
    {
    	var expr = /^((19|20)[0-9][0-9])$/;
		var ret = expr.exec(this.getFieldValue(field_name));
		return !(ret == null);
    },
    
    validBirthdateMonth : function(field_name)
    {
    	var expr = /^(0?[1-9]|1[0-2])$/;
		var ret = expr.exec(this.getFieldValue(field_name));
		return !(ret == null);
    },
    
    validBirthdateDay : function(field_name)
    {
    	var expr = /^(0?[1-9]|[1-2][0-9]|3[0-1])$/;
		var ret = expr.exec(this.getFieldValue(field_name));
		return !(ret == null);
    },
    
    validCPREnd : function(field_name)
    {
    	throw this._newException('CS_ClientValidator_Base.validCPREnd is not implemented');
    },
    
    validBusinessId : function(field_name)
    {
    	throw this._newException('CS_ClientValidator_Base.validBusinessId is not implemented');
    },
    
    validSwift : function(field_name)
    {
    	var expr = /^([A-Za-z]{4}[A-Za-z]{2}[A-Za-z0-9]{2})([A-Za-z0-9]{3})?$/;
		var ret = expr.exec(this._data[field_name].value);
		return !(ret == null);
    },
    
    validIban : function(field_name)
    {
		var expr = /^([A-Za-z0-9 ]{8,30})$/;
		var ret = expr.exec(this._data[field_name].value);
		return !(ret == null);
    },
    
    validLegallyIndependantAge : function(field_name, args)
    {
    	var fields = this._getOverridableFields(args.fields, [
    		'birthdate_year',
        	'birthdate_month',
        	'birthdate_day'
        ]);
    	var year_field	= fields[0];
    	var month_field = fields[1];
    	var day_field 	= fields[2];
    	
    	var year  = this.getFieldValue(year_field);
    	var month;
    	var day;
    	var onlyyear = args.onlyyear || false;
    	
    	if(parseInt(year) <= 0)
    	{
    		return false;
    	}
    	
    	if(!onlyyear)
    	{
    		if(!this._issetField(month_field))
    		{
    			throw this._newException(month_field + ' is undefined, needed for validLegallyIndependantAge');
    		}
    		month = this.getFieldValue(month_field)
    		
    		if(!this._issetField(day_field))
    		{
    			throw this._newException(day_field + ' is undefined, needed for validLegallyIndependantAge');
    		}
    		day = this.getFieldValue(day_field)
    	}
    	
    	if(!month || day <= 0)
    	{
    		month = 12;
    	}
    	
    	if(!day || day <= 0)
    	{
    		day   = 31;
    	}
    	
    	var birthdate = new Date();
    	birthdate.setFullYear(year, (month-1), day);
    	
    	var age_limit = new Date();
    	age_limit.setYear(age_limit.getYear()-18);
    	
    	if(birthdate.getYear() && birthdate <= age_limit)
    	{
    		return true;
    	}
    	
    	return false;
    },
    
    validWhiteList : function(field_name, args)
    {
    	var value = this.getFieldValue(field_name);
    	
    	if(this._indexOf(args.options, value) !== -1)
    	{
    		var chain_values = args.chain_values || {};
    		if(typeof chain_values[value] != 'undefined')
    		{
    			this._validateChain(args.chain_values[value]);
    		}
    		
    		return true;
    	}
    	
    	return false;
    },
    
    validNotEqual : function(field_name, args)
    {
    	var fields = args.fields;
    	var values = [];
    	
    	for(i in fields)
    	{
    		if(fields[i] != field_name)
    		{
   				values.push(this.getFieldValue(fields[i]));
   			}
    	}
    	
   		if(this._indexOf(values, this.getFieldValue(field_name)) === -1)
    	{
    		return true;
    	}
    	
    	return false;
    },
    
    validAgeRange : function(field_name, args)
    {
    	var min_age = false;
    	if(typeof args.min != 'undefined')
		{
			min_age = args.min;
		}
		
		var max_age = false;
    	if(typeof args.max != 'undefined')
		{
			max_age = args.max;
		}
		
		var now = new Date();
		if((!min_age || this.getFieldValue(field_name) <= (now.getFullYear() - min_age))
			&& (!max_age || this.getFieldValue(field_name) >= (now.getFullYear() - max_age)))
		{
			return true;
		}
		return false;
    },
    
    validNumberRange : function(field_name, args)
    {
    	var min_num = false;
    	if(typeof args.min != 'undefined')
		{
    		min_num = args.min;
		}
		
		var max_num = false;
    	if(typeof args.max != 'undefined')
		{
			max_num = args.max;
		}
		
		if(! min_num && ! max_num)
		{
			throw this._newException('validNumberRange requires at least one of options: min and max');
		}

		if((! min_num || this.getFieldValue(field_name) >= min_num)
			&& (! max_num || this.getFieldValue(field_name) <= max_num))
		{
			return true;
		}
		return false;
    },
    
    validNumber : function(field_name)
    {
    	// FIXME add range support
    	return this.getFieldValue(field_name) != '' && !isNaN(this.getFieldValue(field_name));
    },
    
    validRegex : function(field_name, args)
    {
    	if(typeof args.pattern == 'undefined')
    	{
    		throw this._newException('field ' + field_name + ', pattern argument undefined for validRegex');
    	}
    	
    	var pattern = args.pattern;
    	
    	var modifiers = "i";
    	if(typeof args.modifiers != 'undefined')
    	{
    		modifiers = args.modifiers;
    	}
    	
    	// hack to support // wrapper characters in js
    	if(pattern.charAt(0) == '/' && pattern.charAt(pattern.length - 1) == '/')
    	{
    		pattern = pattern.substr(1, pattern.length - 2);
    	}
    	
    	var expr = new RegExp(pattern, modifiers);
		var ret = expr.exec(this.getFieldValue(field_name));
		return !(ret == null);
    },
    
    validNotPostbox : function(field_name)
    {
    	throw this._newException('CS_ClientValidator_Base.validNotPostbox is not implemented');
    },
    
    validWhiteListExplode : function(field_name)
    {
    	throw this._newException('CS_ClientValidator_Base.validWhiteListExplode is not implemented');
    },
    
    /**
     * Validate a chain against data and register any failures
     * @param {Object} chain
     */
    _validateChain : function(chain)
    {
    	if(!chain)
    	{
    		return;
    	}
    	
    	this._chain_depth_count++;
    	
    	if(this._chain_depth_count > this._chain_depth_limit)
    	{
    		throw this._newException('failed to validate chain, max recursion level reached');
    	}
    	
    	if(typeof this._data != 'object')
    	{
    		throw this._newException('form object is undefined');
    	}
    	
    	for(var field_name in chain)
    	{
	    	for(var i = 0; i < chain[field_name].length; i++)
	    	{
	    		var validator = chain[field_name][i];
	    		
	   			if(this._indexOf(this._optional_fields, validator.field_name) !== -1
	   				&& this.isEmpty(this.getFieldValue(validator.field_name)))
	    		{
	    			this._validateChain(validator.chain_optional);
	   				continue;
	    		}
	   			
	   			if(typeof this._data[validator.field_name] == 'undefined')
	   			{
	   				throw this._newException('field "' + validator.field_name + '" is undefined');
	   			}
	   			
	   			if(typeof this[validator.method] == 'undefined')
	   			{
	   				throw this._newException('validator "' + validator.method + '" is undefined');
	   			}
	   			
	    		if(!this[validator.method](validator.field_name, validator.args))
	    		{
	    			this._registerError(validator);
	    			this._validateChain(validator.chain_invalid);
	    		}
	    		else
	    		{
	    			this._validateChain(validator.chain_valid);
	    		}
	    	}
	    }
    	
    	this._chain_depth_count--;
    },
    
    /**
     * @private
     * @param {String} field_name
     * @param {String} method
     * @param {String} error_msg
     * @param {Object} [args]
     */
    _addValidator : function(field_name, method, error_msg, args)
    {
		var stub = new CS_ClientValidator_Base.Stub;
		stub.setFieldName(field_name);
		stub.setMethod(method);
		stub.setErrorMsg(error_msg);
		
		if(typeof args == 'object')
		{
			stub.setChainValid(args.chain_valid);
			delete args.chain_valid;
			
			stub.setChainOptional(args.chain_optional);
			delete args.chain_optional;
			
			stub.setChainInvalid(args.chain_invalid);
			delete args.chain_invalid;
			
			stub.setChainValues(args.chain_values);
			delete args.chain_values;
			
			stub.setArgs(args);
			
			if(validator.chain_values)
			{
				var chain_values = {};
				for(var j in validator.chain_values) {
					chain_values[j] = this._prepareValidatorsArrayChain(validator[k][j]);
				} args.chain_values = chain_values;
				
				stub.setChainValues(validator.chain_values);
			}
			
			if(typeof args.optional == 'boolean' && args.optional == true)
			{
				this._optional_fields.push(field_name);
			}
		}
		
		this._appendChain(this._validators, field_name, stub);
    },
    
    /**
     * @private
     * @param {Array} validators_array
     * @return Object
     */
    _prepareValidatorsArrayChain : function(validators_array)
	{
		var chain = {};
	
		this._chain_depth_count++;
	
		if(this._chain_depth_count > this._chain_depth_limit)
    	{
    		throw this._newException('failed to prepare validators array, max recursion level reached');
    	}
		
		for(var field_name in validators_array)
		{
			var reset_validators = true;
			if(typeof validators_array[field_name]['reset'] != 'undefined')
			{
				if(validators_array[field_name]['reset'] == false)
				{
					reset_validators = false;
				}
			}
			if(reset_validators)
			{
				this._validators[field_name] = [];
			}
			
			for(var i in validators_array[field_name])
			{
				if(i == 'reset')
				{
					continue;
				}
				
				var validator = validators_array[field_name][i];
				var args = {}, chain_values = {};
				
				var stub = new CS_ClientValidator_Base.Stub;
				stub.setFieldName(field_name);
				
				for(var k in validator)
				{
					switch(k)
					{
						case '0': stub.setMethod(validator[k]); break;
						case '1': stub.setErrorMsg(validator[k]); break;
						case 'chain_valid':	stub.setChainValid(this._prepareValidatorsArrayChain(validator[k])); break;
						case 'chain_optional': stub.setChainOptional(this._prepareValidatorsArrayChain(validator[k])); break;
						case 'chain_invalid': stub.setChainInvalid(this._prepareValidatorsArrayChain(validator[k])); break;
						case 'chain_values':
							for(var j in validator.chain_values) {
								chain_values[j] = this._prepareValidatorsArrayChain(validator[k][j]);
							} 
							args.chain_values = chain_values; 
							stub.setChainValues(chain_values);break;
						default: args[k] = validator[k]; break;
					}
				}
				
				stub.setArgs(args);
				
				this._appendChain(chain, field_name, stub);
			}
		}
		
		this._chain_depth_count--;
		
		return chain;
	},
    
    /**
     * Find index of value in array
     * @private
     * @param {Array} array
     * @param {mixed} value
     * @type number
     * @return If value is not found -1 is returned
     */
    _indexOf : function(array, value)
    {
    	for(var i=0; i<array.length; i++)
        {
            if(array[i]==value)
            {
                return i;
            }
        }
        return -1;
    },
    
    /**
     * Check if value is numeric
     * @private
     * @param {String} value
     * @return boolean
     */
    _isNumeric : function(value)
    {
		var expr = /^(-)?(\d*)(\.?)(\d*)$/;
		var ret = expr.exec(value);
		return !(ret == null);
    },
    
    /**
     * Register an error with the stack
     * @private
     * @param {Stub} validator
     */
    _registerError : function(validator)
    {
    	this._errors.push(validator);
		if(typeof this._display_errors[validator.field_name] == 'undefined')
		{
			var error_msg = validator.error_msg.replace(/(<([^>]+)>)/ig, "");
			this._display_errors[validator.field_name] = error_msg;
		}
    },
    
    /**
     * Register an array of errors with the stack
     * @private
     * @param {Array} errors
     */
    _registerErrors : function(errors)
    {
    	for(i in errors)
    	{
    		this._registerError(errors[i]);
    	}
    },
    
    /**
     * Reset the state of the validator (excluding validators),
     * done before each call to isValid
     * @private
     */
    _reset : function()
    {
    	this._chain_depth_count = 0;
    	this._errors 			= [];
    	this._display_errors 	= [];
    },
    
    /**
     * Return new validator exception, to use with throw
     * @private
     * @param {String} msg
     * @type Object
     * @return CS_ClientValidator_Exception
     */
    _newException : function(msg)
    {
    	var ex = new Error(msg);
    	ex.name = 'CS_ClientValidator_Exception';
    	return ex;
    },
    
    /**
	 * Merge array1 and array2 and return the result
	 * @private
	 * @param {Array} array1
	 * @param {Array} array2
	 * @type Array
	 * @return merged array
	 */
    _arrayAppend : function(array1, array2)
    {
    	for(i in array2)
    	{
    		array1.push(array2[i]);
    	}
    	return array1;
    },
    
    /**
	 * Check if field name exists in data
	 * @private
	 * @param {String} field_name
	 */
    _issetField : function(field_name)
    {
    	if(typeof this._data[field_name] != 'undefined')
    	{
    		return field_name;
    	}
    	return false;
    },
    
	/**
	 * Append stub (validator) to a chain
	 * @private
	 * @param {Array} chain
	 * @param {String} field_name
	 * @param {Stub} stub
	 */
	_appendChain : function(chain, field_name, stub)
	{
		if(typeof stub != 'object')
		{
			throw this._newException('cannot append to chain, invalid stub');
		}
	
		if(typeof chain != 'object')
		{
			throw this._newException('cannot append stub, invalid chain');
		}
	
		if(typeof chain[field_name] == 'undefined')
		{
			chain[field_name] = [];
		}
		
		chain[field_name].push(stub);
	},
	
	/**
	 * Merge chain2 into chain one
	 * @private
	 * @param {Object} chain1
	 * @param {Object} chain2
	 */
	_mergeChains : function(chain1, chain2)
	{
		for(field_name in chain2)
		{
			for(var i = 0; i < chain2[field_name].length; i++)
			{
				this._appendChain(chain1, field_name, chain2[field_name][i]);
			}
		}
	},
	
	/**
	 * 
	 */
	_getOverridableFields : function(user_fields, fields)
    {
    	if(typeof user_fields == 'undefined')
    	{
    		return fields;
    	}
    	
    	if(user_fields.length != fields.length)
    	{
    		throw this._newException('argument "fields" has ' + user_fields.length + ' elements, but should have ' + fields.length);
    	}
    	
    	for(var i = 0; i < fields.length; i++)
    	{
    		fields[i] = user_fields[i];
    	}
    	
    	return fields;
    },
	
	/**
	 * Check if value is empty
	 * @private
	 * @param {String} value
	 * @type boolean
	 * @return boolean
	 */
	isEmpty : function(value)
	{
		if(value == '' || value <= 0 || value == null)
		{
			return true;
		}
		return false;
	}
}

/**
 * Stub class for containing each validator
 */
CS_ClientValidator_Base.Stub = function()
{
	
}

CS_ClientValidator_Base.Stub.prototype = 
{
	field_name 		: '',
	method	 		: '',
	error_msg	 	: '',
	args		 	: {},
	chain_valid		: false,
	chain_optional	: false,
	chain_invalid	: false,
	chain_values	: false,
	
	setFieldName : function(field_name) { this.field_name = field_name; },
	getFieldName : function() { return this.field_name; },
	
	setMethod : function(method) { this.method = method; },
	getMethod : function() { return this.method; },
	
	setErrorMsg : function(error_msg) { this.error_msg = error_msg; },
	getErrorMsg : function() { return this.error_msg; },
	
	setArgs : function(args) { this.args = args; },
	getArgs : function() { return this.args; },
	
	setChainValid : function(chain_valid) { this.chain_valid = chain_valid; },
	getChainValid : function() { return this.chain_valid; },
	
	setChainOptional : function(chain_optional) { this.chain_optional = chain_optional; },
	getChainOptional : function() { return this.chain_optional; },
	
	setChainInvalid : function(chain_invalid) { this.chain_invalid = chain_invalid; },
	getChainInvalid : function() { return this.chain_invalid; },
	
	setChainValues : function(chain_values) { this.chain_values = chain_values; },
	getChainValues : function() { return this.chain_values; }
}