// String.length2() is for supporting multi-byte string length.
if (!String.prototype.length2) {
    String.prototype.length2 = function() {
        var cArr = this.match(/[^\x00-\xff]/ig);
        return this.length + (cArr == null ? 0 : cArr.length);
    }
}

/**
 * Validation class.
 *
 * @author Nio Xiao
 */
Validation = Class.create();
Object.extend(Validation.prototype, {
	initialize: function(options) {
		this.setOptions(options);
		this.validators = $H();
		this.errmsgs 	= $H();
		this.observers 	= [];
	},
	
	setOptions: function(options) {
		defaultOptions = {
			stopWhenError: true,
			autoTriggers: true,
			trigger: this.defaultTrigger,
			errorClass: 'error_message'
		};
		Object.extend(Object.extend(this, defaultOptions), options || {});
	},
	
	addValidator: function(field, validator, trigger) {
		if (!this.validators.keys().include(field))
			this.validators[field] = [];
		this.validators[field].push(validator);
		if (arguments.length < 3 && this.autoTriggers) {
			trigger = this.defaultTrigger;
		}
		if (trigger) {	//add a trigger to the text field control.
			this.addTrigger(field, trigger);
		}
	},
	
	addTrigger: function(field, trigger) {
		var o = $(field), tagName = o.tagName.toLowerCase();

		if (typeof trigger != 'function') {
			throw 'Validation trigger for "' + field + '" must be a function.';
		}
		if ( (tagName == 'input' && ['text','password'].indexOf(o.type.toLowerCase()) != -1) ||
			 (tagName == 'textarea') ) {
				if (!this.observers.include(field)) {
					// var v = this;
					Event.observe(o, 'blur', function() { trigger.apply(this, [field]); }.bind(this));
					this.observers.push(field);
				}	//end if
		} else {
			throw 'Trigger for "' + field + '" should be a text input box or a textarea box.';
		}
	},
	
	defaultTrigger: function(field) {
		//parent is a container of field control, and it should be '<p>' or '<div>'.
		// var parent = $(field).parentNode, id = field + 'ErrorField';

		if (this.validate(field)) {
			this.removeInlineError(field);
		} else {
			this.showInlineError(field);
		}	//end if
	},
	
	removeInlineError: function(field) {
		var parent = $(field).parentNode, id = field + 'ErrorField';
		
		Element.removeClassName(parent, this.errorClass);
		if ($(id)) {
			Element.remove(id);
		}		
	},
	
	showInlineError: function(field, errmsg) {
		var parent = $(field).parentNode, id = field + 'ErrorField';
		
		if (!errmsg)
			errmsg = this.errmsgs[field].join('<br/>');
		Element.addClassName(parent, this.errorClass);
		if (!$(id)) {
			new Insertion.Bottom(parent, '<span id="' + id + '"></span>');
		}
		Element.update(id, errmsg);
	},
	
	validate: function(field) {
		var value, keys, error, errmsg = '';

		this.errmsgs = $H();
		keys = (arguments.length == 1) ? [field] : this.validators.keys();
		for (var i = 0, maxi = keys.length; i < maxi; i++) {
			field = keys[i];
			value = ($F(field) || '').strip();
			if (!this.validators.keys().include(field))
				throw 'Could not find the validators by field "' + field + '".';
			vs = this.validators[field];
			if (!value.length2() && !this.isRequired(field)) {
				continue;	//the field is empty and it isn't required.
			}	//end if
			for (var j = 0, maxj = vs.length; j < maxj; j++) {
				v = vs[j];
				error = false;
				if (v.regexp) {
					if (!v.regexp.test(value)) {
						error = true;
						errmsg = v.errmsg;
					}	//end if
				} else if (v.handle) {
					// v.args.unshift(field);alert(v.args);
					if (!v.handle.apply(this, [field].concat(v.args))) {
						error = true;
						errmsg = v.errmsg;
					}
				}	//end if
				if (error) {	//ERROR
					if (!this.errmsgs.keys().include(field))
						this.errmsgs[field] = [];
					this.errmsgs[field].push(errmsg);
					if (this.stopWhenError)
						return false;
				}	//end if
			}	//end for
		}	//end for
		return !this.isError();
	},
	
	isRequired: function(field) {
		return this.validators[field].detect(function(v) {
			return (v.regexp && v.regexp == Validation.Patterns.REQUIRED);
		})
	},
	
	isError: function(field) {
		if (arguments.length == 1) {
			return this.errmsgs.keys().include(field);
		} else {
			return this.errmsgs.keys().length > 0;
		}	//end if
	},
	
	getErrors: function(field) {
		if (arguments.length == 1) {
			return this.errmsgs[field];
		} else {
			return this.errmsgs;
		}	//end if
	},
	
	inspect: function() {
		return '<#Validation:' + $H(this) + '>';
	},
	
	removeErrors: function() {
		this.validators.each(function(field) {
  			var parent = $(field.key).parentNode, id = field.key + 'ErrorField';
			Element.removeClassName(parent, 'error_message');
			if ($(id)) {
				Element.remove(id);
			}
		});
	}
});

Validation.Patterns = {
	REQUIRED : /.+/,
	EMAIL	 : /^([a-z0-9][a-z0-9_\-\.\+]*)@([a-z0-9][a-z0-9\.\-]{0,63}\.(com|org|net|biz|info|name|net|pro|aero|coop|museum|[a-z]{2,4}))$/i,
	DATE	 : /^[12][0-9]{3}[\-\.](0[1-9]|1[0-2])[\-\.](0[1-9]|[12][0-9]|3[01])$/,
	INTEGER	 : /^([0-9]|[1-9][0-9]*)$/
}

Validation.Validator = Class.create();
Object.extend(Validation.Validator.prototype, {
	initialize: function(options) {
		this.setOptions(options);
	},

	setOptions: function(options) {
		defaultOptions = {
			regexp	 : null,
			handle   : null,
			args     : [],
			errmsg   : ''
		};
		Object.extend(Object.extend(this, defaultOptions), options || {});
    }
});

Validation.Validators = {
    isEqual: function(field1, field2) {
        return $F(field1) == $F(field2);
    },

	isNumber: function(field) {
		return !isNaN($F(field));
	},
	
	isDecimal: function(field) {
		return Validation.Validators.isNumber(field);
	},
	
    isInNumberRange: function(field, min, max) {
        var val = parseInt($F(field));
        if (isNaN(val))
            return false;
        if (min != null && val < min)
            return false;
        if (max != null && val > max)
            return false;
        return true;
    },

    isInStringRange: function(field, min, max) {
        var len = $F(field).length2();
        if (min != null && len < min)
            return false;
        if (max != null && len > max)
            return false;
        return true;
    }	
}

$V = new Validation();
$VP = Validation.Patterns;
$VV = Validation.Validators;