﻿(function(ns){
	
	ns.contains = function(v, or){
		v = $splat(v); or = $pick(or, true);
		for(var result = false, i = 0, l = v.length; i < l; i ++){
			result = ((this & v[i]) == v[i]);
			if(result && or || !result && !or) break;
		}
		return result;
	};

	ns.trim = {
		all : function(txt){return (txt || this).replace(/(^[ \s]+)|([ \s]+$)/g, '');},
		left : function(txt){return (txt || this).replace(/^[ \s]+/, '');},
		right : function(txt){return (txt || this).replace(/[ \s]+$/, '');}
	};
	var $properties = "accept,action,as,code,depend,elements,empty,expression,for,format,max,maxElement,min,minElement,operator,options,pass,pattern,prop,regex,require,rule,tips,to,toElement,trim,warn".split(',');

	var $triggers = {
		submit : 1,
		blur : 2,
		any : 3
	};

	var $regexs = {
		require : /.+/,
		email : /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/,
		currency : /^\d+(\.\d+)?$/,
		number : /^\d+$/,
		zip : /^[1-9]\d{5}$/,
		qq : /^[1-9]\d{4,8}$/,
		english : /^[A-Za-z]+$/,
		chinese :  /^[\u0391-\uFFE5]+$/,
		username : /^[a-z]\w{3,19}$/i,
		integer : /^[-\+]?\d+$/,
		'double' : /^[-\+]?\d+(\.\d+)?$/
	};
	
	var util = {
		numberFilter : function(input){
			return input.replace(/\D/g, '');
		},
		ipFix : function(input){
			return $regexs.ip.test(input)?input.replace(/([^\d])(\d{1,2})(?=\.|\b)/g, '$100$2') : false;
		},
		comma : /\s*[,，]\s*/
	};
	
	var AbstractMessenger = new Class({
		Implements : Options,
		options : {
			warn : null,
			tips : null,
			pass : null,
			remote : null
		},
		initialize : function(form, options){
			this.form = form;
			this.setOptions(options);
			this.bound = {
				'onSuccess' : this.showPass.bind(this),
				'onFailure' : this.showWarn.bind(this),
				'onTips' : this.showTips.bind(this)
			};
			if (this.options.initialize) this.options.initialize.call(this);
		},
		showWarn : function(o){this.process(o, 'warn');},
		showTips : function(o){o.element.title = this.chk(o, 'tips') ? o.options.tips : '';},
		showPass : function(o){if(o !== this.form) this.process(o, 'pass');},
		process : function(o, key){},
		chk : function(o, key){return o.options && o.options[key] && o.options[key] != ''}
	});
	
	ns.FollowMessenger = new Class({
		Extends : AbstractMessenger,
		options : {
			warn : 'msg block',
			tips : 'msg tips',
			remote : 'msg loading',
			pass : 'msg pass',
			blur : 'msg'
		},
		initialize : function(form, options){
			arguments.callee.parent(form, options);
			this.bound.onRemote = this.showRemote.bind(this);
		},
		showTips : function(o, isBlur){if(o !== this.form && this.chk(o, 'tips')) o.getMessagePanel().setProperties({html : o.options.tips, 'class' : this.options[isBlur ? 'blur' : 'tips']});},
		showRemote : function(o){o.getMessagePanel().setProperties({html : '\u6b63\u5728\u8fdc\u7a0b\u9a8c\u8bc1\uff0c\u8bf7\u7a0d\u5019...', 'class' : this.options.remote}).setStyle('visibility', 'visible');},
		process : function(o, key){
			$splat(o).each(function(e){
				var msg = e.getMessage();
				e.getMessagePanel().setProperties({html : msg, 'class' : this.options[key]}).setStyle('visibility', msg && msg != '' ? 'visible' : 'hidden');
			}.bind(this));
		}
	});
		
	var AbstractReader = new Class({
		Implements : [Events, Options],
		options : {
			wait : true
		},
		initialize : function(form, options){
			this.form = form;
			this.setOptions(options);
			if (this.options.initialize) this.options.initialize.call(this);
			if(this.options.wait) window.addEvent('domready', this.read.bind(this));
			else this.read();
		},
		read : function(){},
		fill : function(){}
	});
	
	ns.AttributeReader = new Class({
		Extends : AbstractReader,
		read : function(){
			this.form.getForm().getElements('input[rule], select[rule], textarea[rule]', true).each(function(el){
				if (!el.name || el.disabled) return;
				this.fill(el);
			}.bind(this));
		},
		fill : function(el){
			this.form.addElement(ns.Factory.Element.build(ns.Factory.Element.getAttributes(el)));
		}
	});		
	var Validator = ns.Validator = {
		forms : {},
		setup : function(options){
			var form = new ns.Form(options);
			this.forms[options.form] = form;
			return form;
		},
		validate : function(options){
		},
		registerElement : function(className, prototype, autoInherit){
			if(autoInherit !== false) prototype.Extends = AbstractElement;
			ns.ExtendElements[className] = new Class(prototype);
		},
		registerRegex : function(key, regex){
			$regexs[key] = regex;
		},
		registerProperty : function(){
			for(var i = 0, l = arguments.length, ps = $properties.join(','); i < l; i ++) if(!ps.contains(arguments[i], ',')) $properties.push(arguments[i]);
		},
		registerReader : function(className, prototype, autoInherit){
			if(autoInherit !== false) prototype.Extends = AbstractReader;
			ns.ExtendReaders[className] = new Class(prototype);
		},
		registerMessenger : function(className, prototype, autoInherit){
			if(autoInherit !== false) prototype.Extends = AbstractMessenger;
			ns.ExtendMessengers[className] = new Class(prototype);
		},
		toString : function(){return ['Version: 4.0.0 beta', 'Author: \u6211\u4f5b\u5c71\u4eba', 'Email: wfsr-at-msn.com'].join('\n');}
	};
	
	ns.Factory = {
		Element : {
			build : function(o){
				var element, rule = o.rule.capitalize().replace(/Element$/, '') + 'Element';
				return new ($pick(ns[rule], ns.ExtendElements[rule], ns.RegexElement))(o);
			},
			getAttributes : function(el){
				var options = {element : el};
				$properties.each(function(prop){
					if(el.getAttribute(prop)) options[prop] = el.getAttribute(prop);
				});
				return options;
			}
		},
		Messenger : {
			build : function(name, form, options){
				name = name.capitalize().replace(/Messenger$/, '') + 'Messenger';
				return new ($pick(ns[name], ns.ExtendMessengers[name]))(form, options);
			}
		},
		Reader : {
			build : function(name, form, options){
				name = name.capitalize().replace(/Reader$/, '') + 'Reader';
				return new ($pick(ns[name], ns.ExtendReaders[name]))(form, options);
			}
		}
	};
	
	ns.Form = new Class({
		Implements : [Events, Options],
		options : {
			form : null,
			configs : 'tag',
			triggers : 'submit, blur',
			warns : 'follow',
			step : false,
			wait : false,
			title : '\u63d0\u4ea4\u5931\u8d25\uff0c\u8bf7\u6309\u4ee5\u4e0b\u63d0\u793a\u68c0\u67e5\u60a8\u7684\u8f93\u5165\uff1a',
			ignoreOldEvent : true,
			events : null/*,
			onTips : $empty,
			onRemote : $empty,
			onSuccess : $empty,
			onFailure : $empty*/
		},
		initialize : function(options){
			this.isValid = false;
			this.elements = [];
			this.invalids = [];
			this.batch = false;
			this.setOptions(options);
			this.initMessenger();
			this.parseEnmValue('triggers', $triggers);
			this.bound = {onTips : this.tips.bind(this), onRemote : this.remote.bind(this), onFailure : this.failure.bind(this), onSuccess : this.success.bind(this)};
			if (this.options.initialize) this.options.initialize.call(this);
			if(this.options.wait) window.addEvent('domready', this.loadConfigs.bind(this));
			else this.loadConfigs();
		},
		validate : function(){
			this.invalids.empty();
			this.batch = true;
			for(var i = 0, l = this.elements.length; i < l; i ++) if(!this.elements[i].validate() && this.options.step) break;
			(this.isValid = this.invalids.length == 0) ? this.fireEvent('onSuccess', this) : this.fireEvent('onFailure', [this.invalids]);
			this.batch = false;
			return this.isValid;
		},
		tips : function(el, isBlur){
			this.fireEvent('onTips', [el, isBlur]);
		},
		remote : function(el){
			this.fireEvent('onRemote', el);
		},
		success : function(el){
			this.fireEvent('onSuccess', el);
		},
		failure : function(el){
			this.invalids.include(el);
			if(this.options.step || !this.batch) this.fireEvent('onFailure', el);
		},
		initMessenger : function(){
			var self = this;
			this.options.warns.split(util.comma).each(function(warn){
				warn = warn.trim().toLowerCase();
				var messenger = ns.Factory.Messenger.build(warn, self, self.options[warn] || {});
				if(messenger)  self.addEvents(messenger.bound);
			});
		},
		loadConfigs : function(){
			var self = this;
			this.options.configs.split(util.comma).each(function(config, i){
				config = config.trim().toLowerCase();
				ns.Factory.Reader.build(config, self, self.options[config] || {});
			});
			if(Browser.loaded) this._add();
			else window.addEvent('domready', this._add.bind(this));
		},
		getForm : function(){
			if(!this.form){
				this.form = $(this.options.form);
				if(ns.contains.call(this.options.triggers, $triggers.submit)){
					if(this.options.ignoreOldEvent){
						this.form.removeProperty('onsubmit');
						this.form.onsubmit = null;
					} else {
						var oldEvent = this.form.onsubmit;
					}
					this.form.addEvent('submit', function(){
						if(oldEvent) oldEvent();
						return this.validate();
					}.bind(this));
				}
			}
			return this.form;
		},
		getFormItem : function(name){
			var items = this.getForm().getElements('[name=' + name + ']');
			return items.length ? items[items.length - 1] : $(name);
		},
		addElement : function(options){
			var ops = this.options;
			var element = options;
			this.elements.push(element);
			element.addEvents(this.bound);
			if(ops.events && ops.events[element.element.name]) element.addEvents(ops.events[element.element.name]);
			element.attach(ns.contains.call(ops.triggers, $triggers.blur));
		},
		add : function(o, loaded){
		   if(!loaded) {
			this.rules = this.rules || [];
			this.rules.push(o);
		   } else {
			if(!o.validate) o = ns.Factory.Element.build(o);
			this.addElement(o);
		   }
		   return this;
		  },
		  _add : function(){
		   if(!this.rules)return;
		   for(var i = 0, l = this.rules.length; i < l; i ++) this.add(this.rules[i], true);
		   this.rules.empty();
		   this.rules = null;
		   delete this.rules;
		  },
		parseEnmValue : function(prop, enums){
			var ops = this.options;
			if(typeof(ops[prop]) == 'string'){
				var properties = ops[prop];
				ops[prop] = 0;
				properties.replace(/\s/g, '').split(',').each(function(key){
					ops[prop] |= enums[key] || 0;
				});
				ops[prop] = ops[prop] || 1;
			}
		}
	});
	
	var AbstractElement = new Class({
		Implements : [Events, Options],
		options : {
			element : null,
			trim : 'all',
			depend : false,
			require : true,
			warn : null,
			tips : null,
			empty : null,
			pass : null,
			action : null/*,
			onTips : $empty,
			onRemote : $empty,
			onSuccess : $empty,
			onFailure : $empty*/
		},
		initialize : function(options){
			this.isValid = false;
			this.setOptions(options);
			this.options.require = this.toBoolean(this.options.require);
			this.element = $(this.options.element || this.options['for']);
			delete this.options.element;
			if (this.options.initialize) this.options.initialize.call(this);
		},
		attach  : function(validateOnBlur){
			this.bound = {focus : this.focus.bind(this), 'blur' : this.blur.bind(this, validateOnBlur)};
			if(Browser.Engine.webkit && this.element.type == 'file') {this.bound.change = this.bound.blur;delete this.bound.blur;}
			this.getMessagePanel();
			this.fireEvent('onTips', [this, true]);
			this.element.addEvents(this.bound);
		},
		focus : function(){
			this.fireEvent('onTips', this);
		},
		blur : function(validate){
			if(!validate) {this.fireEvent('onTips', [this, true]);return;}
			this.validate();
		},
		beforeRemote : function(){},
		afterRemote : function(json){
			this.isValid = (json.state == 1);
			this.$json = json;
			this.$value = this.value;
			this.fireEvent(this.isValid ? 'onSuccess' : 'onFailure', this);			
		},
		remoteValidate : function(){
			if(this.isValid && this.isRemote() && this.value != (this.$value || '')) {
				this.params = {};
				this.params[this.element.name] = this.value;
				this.fireEvent('onRemote', this);
				var remote = new Request.JSON({url:this.options.action, onSuccess : function(json){
					this.afterRemote(json);
					remote = null;
				}.bind(this),
				onFailure : function(){
					this.afterRemote({state : 1, message : '\u62b1\u6b49\uff0c\u8fdc\u7a0b\u8fde\u63a5\u51fa\u9519\uff0c\u4f46\u662f\u60a8\u53ef\u4ee5\u6b63\u5e38\u63d0\u4ea4\u672c\u9879'});
					remote = null;
				}.bind(this)}
				).post(this.params);
			}
		},
		success  : function(){
			if(this.options.require || this.value != '') {
				this.fireEvent('onSuccess', this);
			}
		},
		failure  :  function(){
			this.fireEvent('onFailure', this);
		},
		getValue : function(){
			return this.trim(this.element.get('value') || '');
		},
		getMessage : function(){
			return (this.$json ? this.$json.message : this.options[this.isValid ? 'pass' : (this.value === '' ? 'empty' : 'warn')]) || '\28\u672a\u914d\u7f6e\u63d0\u793a\u4fe1\u606f\29';
		},
		trim : function(text){
			var fn = ns.trim[this.options.trim];
			return fn ? fn.call(text) : text;
		},
		getMessagePanel : function(){
			if(!this.panel && !(this.panel = $(this.element.name + '_message'))){
				this.panel = new Element('div', { id : this.element.name + '_message'});
				if(this.options.holder) this.panel.injectBefore($(this.options.holder));
				else this.panel.injectAfter(this.element);
			}
			return this.panel;
		},
		validate : function(){
			if(this.element.disabled) return true;
			var ops = this.options;
			this.value = this.getValue();
			if(!(this.$value != undefined && this.value == this.$value)){
				if(ops.depend){try{eval(ops.depend);}catch(e){alert(e);}};
				this.isValid = !ops.require && this.value == '';
				if(!this.isValid){
						this.$json = null;
						this.isValid = this.process();
						if(this.isValid && this.isRemote()) this.remoteValidate();
				}
			}
			this.$value = this.value;
			this.isValid ? this.success() : this.failure();
			return this.isValid;
		},
		isRemote : function(){
			return this.options.action && this.options.action != '';
		},
		process : function(){
			return false;
		},
		toBoolean : function(i){
			return i === true || i === false ? i : 'no,0,false'.indexOf(i.toString().toLowerCase()) == -1;
		}
	});

	ns.RegexElement = new Class({
		Extends : AbstractElement,
		options : {
			pattern : null,
			options : ''
		},
		process : function(){
			this.pattern = this.options.pattern && new RegExp(this.options.pattern, this.options.options) || $regexs[this.options.rule];
			return this.pattern != null && this.pattern.test(this.value);
		}
	});
	
	
	ns.RangeElement = new Class({
		Extends : AbstractElement,
		options : {
			min : 0,
			minElement : null,
			max : 1000,
			maxElement : null,
			format : 'yyyy-MM-dd',
			as : 'number'
		},
		types : {
			string : function(){this.options.format = null;this.min = this.options.min;this.max = this.options.max;},
			caseinsensitivestring : function(){this.value = this.value.toUpperCase();this.min = this.options.min.toUpperCase();this.max = this.options.max.toUpperCase();},
			number : function(){this.value = this.value*1;this.min = this.options.min*1;this.max = this.options.max * 1;},
			datetime : function(){this.value = ns.isDateTime.call(this.value, this.options.format, true);this.min = ns.isDateTime.call(this.options.min, this.options.format, true);this.max = ns.isDateTime.call(this.options.max, this.options.format, true);},
			ip : function(){this.value = util.ipFix(this.value);this.min = util.ipFix(this.options.min);this.max = util.ipFix(this.options.max);}
		},
		process : function(){
			var ops = this.options;
			if(ops.minElement != null) ops.min = $(ops.minElement).value;
			if(ops.maxElement != null) ops.max = $(ops.maxElement).value;
			this.types[ops.as.toLowerCase()].call(this);
			return (this.value === false || this.min === false || this.max === false) ? false : this.min <= this.value && this.value <= this.max;
		}
	});
	ns.LimitElement = new Class({
		Extends : ns.RangeElement,
		options : {as : 'number'},
		getValue : function(){
			return arguments.callee.parent().length;
		}
	});
	
	ns.LimitBElement = new Class({
		Extends : ns.RangeElement,
		options : {as : 'number'},
		getValue : function(){
			return arguments.callee.parent().replace(/[^\x00-\xff]/g,"**").length;
		}
	});
	ns.ExtendElements = {};
	ns.ExtendMessengers = {};
	ns.ExtendReaders = {};
	
})(dengji = {});