/**
 * tFormer.js - empower your forms
 * http://tjrus.com/tFormer
 * (c) 2013 Vasiliy Zubach (aka TjRus) - http://tjrus.com/
 * tFormer may be freely distributed under the MIT license.
 */

(function ( window, document, undefined ){

	// button types
	var BUTTON_TYPES = ['button', 'submit'];
	// all field types that can be empowered with tFormer
	var FIELD_TYPES = ['text', 'password', 'select-one', 'checkbox', 'textarea', 'range', 'number', 'email', 'hidden', 'file', 'date', 'datetime-local', 'search', 'tel', 'time', 'url', 'month', 'week'];
	// fields that has only 'change' event
	var HAS_CHANGE_EVENT = ['checkbox', 'select-one', 'select', 'file'];
	// fields that has also 'change' event
	var CHANGE_ALSO = ['number', 'date', 'datetime-local', 'time', 'month', 'week', 'range'];

	// tFormer options
	var TF_OPTIONS = ['timeout', 'requestTimeout', 'errorClass', 'disabledClass', 'processingClass', 'validateEvent', 'submitButtonControl', 'submitButton', 'submit', 'before', 'onerror', 'onvalid', 'eventBefore', 'eventError', 'eventValid'];
	var FIELD_OPTIONS = ['timeout', 'requestTimeout', 'request', 'validClass', 'errorClass', 'disabledClass', 'processingClass', 'rules', 'validateEvent', 'before', 'onerror', 'onvalid', 'own', 'eventBefore', 'eventError', 'eventValid'];
	var BUTTON_OPTIONS = ['disabledClass', 'processingClass'];


	var EMPOWERED = 'empowered';

	var defaults = {
		errorClass:			'error',
		processingClass:	'processing',
		disabledClass:		'disabled',
		validClass:			'valid',

		eventBefore:		'tFormer:before',
		eventError:			'tFormer:error',
		eventValid:			'tFormer:valid',

		timeout:			0,
		requestTimeout:		2000,

		validateEvent:		'input keyup',

		fields:				{},
		buttons:			{},

		submitButtonControl: true,
		submitButton:		null
	};

	/**
	 * Main tFormer constructor
	 * @param form_el - our form that should be empowered
	 * @param options - tFormer options
	 * @returns {tFormer}
	 */
	var tFormer = function ( form_el, options ){
		if ( !(this instanceof tFormer) ) {
			return new tFormer( form_el, options );
		}

		var self = this;
		// our main form DOM element
		self.form = (function ( form_el ){
			form_el = typeof(form_el) === 'string' ? document.forms[form_el] : form_el;
			return __isForm( form_el ) ? form_el : null;
		})( form_el );

		var my_form = self.form;
		if ( self.form === null ) {
			return null;
		}

		if ( !__getAttr( my_form, EMPOWERED ) ) {
			__setAttr( my_form, EMPOWERED, 1 );
			__setAttr( my_form, 'novalidate', 'novalidate' );
		} else {
			var cached = (function (){
				for ( var i = 0, c_l = self.cache.length; i < c_l; i++ ) {
					if ( self.cache[i].form == my_form ) {
						return self.cache[i];
					}
				}
				return null;
			})();
			if ( cached ) {
				var initialized = cached.inited;
				cached.set( options );
				return cached;
			}
		}
		self.config = (self.config || __clone( defaults ));
		self.set( options );

		self.init();
		self.cache.push( self );
		return self;
	};
	var tf_proto = tFormer.prototype;
	tf_proto.cache = [];


	tf_proto.init = function (){
		var self = this;
		if ( self.inited ) {
			return self;
		}

		self.fields = {};
		self.buttons = {};

		self.inited = true;

		self.locked = 0;
		self.holded = 0;
		self.invalid = 0;
		self.valid = true;

		self.fields = {};
		self.buttons = {};

		// XHR stuff
		self.xhr = {};
		self.xhrTimeout = {};

		var sb = self.get( 'submitButton' );
		if ( sb ) {
			self.buttons['submit'] = new tButton( self, sb );
		}

		for ( var i = 0, f_l = self.form.length; i < f_l; i++ ) {
			var el = self.form[i],
				type = el.type,
				name = type !== 'button' ? __getAttr( el, 'name' ) : __data( el, 'check' );

			if ( __inArray( FIELD_TYPES, type ) !== -1 && name ) {
				self.fields[name] = new tField( self, el );
			}

			if ( __inArray( BUTTON_TYPES, type ) !== -1 ) {
				name = (type == 'submit') ? type : name;
				if ( name && !(name == 'submit' && self.button[name]) ) {
					self.buttons[name] = new tButton( self, el );
				}
			}
		}

		self.form.onsubmit = (function ( self ){
			return function ( event ){
				event = event || window.event;

				var sb = self.button( 'submit' ),
					sb_control = self.get( 'submitButtonControl' ),
					s_func = typeof self.get( 'submit' ) == 'function',
					processing = sb.get( 'processingClass' ),
					prevent = self.valid && s_func;

				// disable double submit
				if ( (processing && sb.hasClass( processing )) || self.locked ) {
					__prevent( event );
					return false;
				}

				if ( prevent ) {
					__prevent( event );
				}
				if ( self.valid ) {
					if ( sb_control ) {
						sb.processing( true );
					}
					if ( s_func ) {
						self.execute( self.form, 'submit', [event, self] );
					}
				}
				if ( prevent ) {
					return false;
				}

				try {
					if ( !self.valid && !self.validate( { no_timeout: true } ) ) {
						__prevent( event );
						sb.processing( false );
						return false;
					}

					if ( s_func ) {
						__prevent( event );
						self.execute( self.form, 'submit', [event, self] );
						return false;
					}
					self.form.submit();
					return true;

				} catch ( e ) {
				}
			};
		})( self );


		self.validate( {
			highlight : false,
			fire_event: false,

			silence: true
		} );
		return self.submitControl();
	};


	tf_proto.destroy = function (){
		var self = this;
		for ( var name in self.fields ) {
			self.fields[name].destroy();
		}
		for ( var name in this.buttons ) {
			self.buttons[name].destroy();
		}
		self.inited = false;
		return self;
	};

	//	function for dropping options
	tf_proto.drop = function (){
		var self = this,
			fields = self.fields,
			buttons = self.buttons;
		self.destroy();
		self.set( __clone( defaults ) );
		for ( var name in fields ) {
			fields[name].drop();
		}
		for ( var name in buttons ) {
			buttons[name].drop();
		}
		self.locked = 0;
		return self.init();
	};

	tf_proto.validate = function ( options ){
		var self = this,
			fields = self.fields,
			errors = 0;

		for ( var key in fields ) {
			errors += (fields[key].validate( options || {} )) ? 0 : 1;
		}
		self.invalid = errors;
		self.valid = errors === 0;
		return errors === 0;
	};


	tf_proto.toObject = function (){
		var self = this,
			fields = self.form,
			obj = {};

		for ( var i = 0, f_l = fields.length; i < f_l; i++ ) {
			var el = fields[i],
				name = __getAttr( el, 'name' );
			if ( el.type == 'checkbox' ) {
				obj[name] = el.checked;
			} else if ( el.type == 'radio' ) {
				if ( !obj[name] ) {
					obj[name] = '';
				}
				if ( el.checked ) {
					obj[name] = el.value;
				}
			} else if (el.type !== 'submit' && el.type !== 'button'){
				obj[name] = el.value;
			}
		}
		return obj;
	};

	tf_proto.get = function ( option ){
		return this.config[option];
	};

	tf_proto.set = function ( options ){
		var self = this,
			is_inited = self.inited;
		if ( is_inited ) {
			self.destroy();
		}
		self.config = __extend( self.config, options );
		if ( is_inited ) {
			self.init();
		}
		return self;
	};

	/**
	 * Get field object to work with
	 * @param {string} name - field name
	 * @returns {*} - field Object
	 */
	tf_proto.field = function ( name ){
		return this.fields[name];
	};

	/**
	 * Get button object to work with
	 * @param {string} name - button name
	 * @returns {*} - button Object
	 */
	tf_proto.button = function ( name ){
		return this.buttons[name];
	};

	/**
	 * Rewrite default form submit function with current one
	 * (default HTML form submit function will be prevented)
	 * @param {function} func - function that should be executed on form submit
	 * @returns {*}
	 */
	tf_proto.submit = function ( func ){
		var self = this;
		if ( !self.config ) {
			return self;
		}
		if ( typeof func == 'function' ) {
			self.config.submit = func;
		}
		return self;
	};

	/**
	 * Form Submit button control function
	 * (enabling/disabling while validating)
	 * @param {!boolean} valid - is current form valid or not?
	 * @returns {*}
	 */
	tf_proto.submitControl = function ( valid ){
		var self = this,
			sb = self.button( 'submit' ),
			sb_control = self.get( 'submitButtonControl' );

		self.valid = (self.invalid === 0 && self.holded === 0 && self.locked === 0);
		valid = (valid === false || valid === true) ? valid : self.valid;

		if ( sb && sb_control ) {
			sb[(valid) ? 'enable' : 'disable']();
		}
		return self;
	};
	/**
	 * Submit button disable
	 * @returns {*}
	 */
	tf_proto.submitDisable = function (){
		return this.submitControl( false );
	};
	/**
	 * Submit button enable
	 * @returns {*}
	 */
	tf_proto.submitEnable = function (){
		return this.submitControl( true );
	};

	/**
	 * Submit button processing control
	 * @returns {*}
	 */
	tf_proto.processing = function ( action ){
		var self = this,
			sb = self.button( 'submit' );
		if ( sb ) {
			sb.processing( action );
		}
		return self;
	};

	/**
	 * Lock form
	 * @returns {*}
	 */
	tf_proto.lock = function ( num ){
		var self = this;
		self.locked += num || 1;
		return self.submitControl( false );
	};

	/**
	 * Unlock form
	 * @returns {*}
	 */
	tf_proto.unlock = function ( num ){
		var self = this;
		self.locked -= num || 1;
		return self.submitControl();
	};

	/**
	 * Execute function
	 * @param context
	 * @param func
	 * @param params
	 * @returns {*}
	 */
	tf_proto.execute = function ( context, func, params ){
		var self = this;
		if ( typeof func == 'string' ) {
			func = self.get( func );
		}
		if ( typeof func == 'function' ) {
			return func.apply( context, (params || []) );
		}
		return null;
	};

	window.tFormer = tFormer;


	/**
	 * Form element constructor
	 * @returns {*}
	 * @constructor
	 */
	var Element = function (){
	};
	var El_p = Element.prototype;
	El_p.destroy = function (){
		var self = this,
			el = this.el,
			events = self.events;

		for ( var i = 0, e_l = events.length; i < e_l; i++ ) {
			self.off( events[0][0], events[0][1] );
		}

		self.removeClass( self.get( 'errorClass' ) );
		self.removeClass( self.get( 'desabledClass' ) );
		self.removeClass( self.get( 'processignClass' ) );

		__data( el, 'holded', null );
		__data( el, 'error', null );
		// remove hold attributes

		if ( self.timer ) {
			clearTimeout( self.timer );
		}
		if ( self.xhr ) {
			self.xhr.abort();
		}
		if ( self.xhrTimeout ) {
			clearTimeout( self.xhrTimeout );
		}

		if ( !self.valid ) {
			self.parent.invalid = self.parent.invalid !== 0 ? self.parent.invalid - 1 : 0;
		}

		self.parent.submitControl();

		self.inited = false;
		return self;
	};

	El_p.drop = function (){
		return this.set( __clone( defaults ) );
	};

	El_p.set = function ( options ){
		var self = this,
			name = __getAttr( self.el, 'name' ),
			_set = function ( key, value ){
				self.config[key] = value;
				if ( !self.parent.config.fields[name] ) {
					self.parent.config.fields[name] = __clone( defaults );
				}
				self.parent.config.fields[name][key] = value;
			},
			is_inited = self.inited;

		if ( is_inited ) {
			self.destroy();
		}
		for ( var key in options ) {
			if ( ~__inArray( FIELD_OPTIONS, key ) ) {
				_set( key, options[key] );
			}
		}
		if ( is_inited ) {
			self.init();
		}
		return self;
	};

	El_p.get = function ( option ){
		return this.config[option];
	};

	/**
	 * subscribe this.el to some event
	 * @param {string} evnt - event name
	 * @param {function} func - function that should be executed on event
	 * @param {object} el - element to attach event
	 * @returns {*}
	 */
	El_p.on = function ( evnt, func, el ){
		var self = this,
			el = el || self.el,
			events = evnt.split( ' ' );

		for ( var i = 0, e_l = events.length; i < e_l; i++ ) {
			if ( el.addEventListener ) { // W3C DOM
				el.addEventListener( events[i], func, false );
			} else if ( el.attachEvent ) { // IE DOM
				el.attachEvent( "on" + events[i], func );
			} else { // No much to do
				el[events[i]] = func;
			}
			self.events.push( [events[i], func] );
		}
		return self;
	};

	/**
	 * unsubscribe this.el (Button || Field) from some event
	 * @param {string} evnt - event name
	 * @param {function} func - function that should be executed on event
	 * @returns {*}
	 */
	El_p.off = function ( evnt, func ){
		var self = this,
			el = self.el,
			events = self.events;

		evnt = evnt.split( ' ' );
		if ( !func ) {
			for ( var i = 0, e_l = events.length; i < e_l; i++ ) {
				if ( __inArray( evnt, events[i][0] ) !== -1 ) {
					events.splice( i, 1 );
				}
			}
			return self;
		}

		if ( el.removeEventListener ) { // W3C DOM
			el.removeEventListener( evnt, func, false );
		} else if ( el.detachEvent ) { // IE DOM
			el.detachEvent( "on" + evnt, func );
		} else { // No much to do
			el[evnt] = null;
		}

		for ( var i = 0, e_l = events.length; i < e_l; i++ ) {
			if ( __inArray( evnt, events[i][0] ) !== -1 && events[i][1] == func ) {
				events.splice( i, 1 );
				return self;
			}
		}
		return self;
	};

	/**
	 * fire some event for this.field
	 * @param {string} evnt - event name
	 * @returns {*}
	 */
	El_p.trigger = function ( evnt ){
		var self = this,
			el = self.el,
			evt;

		if ( self.silence ) {
			return;
		}

		try {// every browser except IE8 and below works here
			evt = document.createEvent( "HTMLEvents" );
			evt.initEvent( evnt, true, true );
			return !el.dispatchEvent( evt );
		} catch ( err ) {
			try {
				return el.fireEvent( 'on' + evnt );
			} catch ( error ) {
			}
		}
		return self;
	};


	El_p.addClass = function ( new_class ){
		var self = this,
			el = self.el,
			class_names = el.className.split( ' ' ) || [];

		if ( __inArray( class_names, new_class ) === -1 ) {
			class_names.push( new_class );
			class_names = __clear( class_names );
			el.className = (class_names.length > 0) ? class_names.join( ' ' ) : '';
		}
		return self;
	};

	El_p.removeClass = function ( old_class ){
		var self = this,
			el = self.el;

		if ( self.hasClass( old_class ) ) {
			var re = new RegExp( '(\\s|^)' + old_class + '(\\s|$)' );
			el.className = el.className.replace( re, ' ' );
		}
		return self;
	}

	El_p.hasClass = function ( name ){
		return !!((~(' ' + this.el.className + ' ').indexOf( ' ' + name + ' ' )));
	};

	El_p.data = function ( attr, value ){
		var self = this,
			el = self.el,
			result = __data( el, attr, value );

		if ( value === undefined ) {
			return result;
		}
		return self;
	};

	El_p.attr = function ( attr, value ){
		var self = this,
			el = self.el;
		switch ( value ) {
			case null:
				__delAttr( el, attr );
				break;
			case undefined:
				return __getAttr( el, attr );
				break;
			default:
				__setAttr( el, attr, value );
				break;
		}
	};

	El_p.processing = function ( action ){
		var self = this,
			processingClass = self.get( 'processingClass' ),
			is_processing = self.hasClass( processingClass );

		if ( action === false || (action === null && is_processing) ) {
			self.removeClass( processingClass );
		} else if ( action === true || (action === null && is_processing) ) {
			self.addClass( processingClass );
		}
		return this;
	};


	/** extent subclass with superclass prototype */
	var __extend_proto = function ( Child, Parent ){
		var F = function (){
		}
		F.prototype = Parent.prototype
		Child.prototype = new F()
		Child.prototype.constructor = Child
		Child.superclass = Parent.prototype
	};


	var tField = function ( parent, el ){
		var self = this,
			type = el.type,
			name, rules,
			attr_required, attr_min, attr_max,
			rules2add = [],
			config;

		self.parent = parent;
		self.el = el;
		self.config = {};

		self.events = [];
		name = self.attr( 'name' );
		attr_required = self.attr( 'required' ) !== null;
		attr_min = self.attr( 'min' );
		attr_max = self.attr( 'max' );

		config = parent.config.fields ? parent.config.fields[name] : {};
		if ( typeof config == 'string' ) {
			config = {
				rules: config
			};
		}
		self.set( __extend( __clone( parent.config ), __clone( config ) ) );
		self.config.rules = self.config.rules || self.data( 'rules' );

		if ( attr_required ) {
			rules2add.push( '*' );
		}

		if ( type == 'email' ) {
			rules2add.push( '@' );
		}

		if ( type == 'url' ) {
			rules2add.push( 'url' );
		}

		if ( type == 'number' ) {
			rules2add.push( 'num' )

			if ( attr_min !== null ) {
				rules2add.push( '>' + attr_min );
			}
			if ( attr_max !== null ) {
				rules2add.push( '<' + attr_max );
			}
		}


		if ( rules2add.length > 0 ) {
			self.config.rules = _v_( '' ).rules( self.config.rules || self.data( 'rules' ) ).addRule( rules2add.join( ' ' ) ).rule;
		}

		self.value = el.value;

		// validate after init
		self.validationStart = 'v_start';
		self.validationSuccess = 'v_success';
		self.validationError = 'v_error';

		self.highlight = true;
		self.fire_event = true;
		self.silence = false;

		return self.init();
	};
	__extend_proto( tField, Element );
	var tField_p = tField.prototype;

	tField_p.init = function (){
		var self = this,
			field = self.el,
			value = field.value,
			type = self.attr( 'type' ),
			is_checkbox = type == 'checkbox';

		if ( self.inited ) {
			return self;
		}

		self.events = [];

		self.valid = true;
		self.holded = false;

		self.value = value;

		// adding validate event to the field;
		var validate_event = self.get( 'validateEvent' );

		if ( ~__inArray( CHANGE_ALSO, type ) && validate_event.indexOf( 'change' ) === -1 ) {
			validate_event = validate_event.split( ' ' );
			validate_event.push( 'change' );
			validate_event = validate_event.join( ' ' );
		}
		if ( ~__inArray( HAS_CHANGE_EVENT, type ) ) {
			validate_event = 'change';
		}

		self.set( {
			validateEvent: validate_event
		} );

		self.on( validate_event, function (){
			if ( !self.get( 'rules' ) ) {
				return;
			}
			if ( self.value != self.el.value || is_checkbox ) {
				self.value = self.el.value;

				// clear field before validation
				self.removeClass( self.get( 'errorClass' ) );
				self.removeClass( self.get( 'processingClass' ) );

				// disable submit button before validation
				self.parent.invalid += (self.valid) ? 1 : 0;
				self.valid = false;
				self.removeClass( self.get( 'errorClass' ) );
				self.parent.submitControl();

				var timeout = self.get( 'timeout' );
				if ( self.get( 'timeout' ) > 0 ) {
					if ( self.timer ) {
						clearTimeout( self.timer );
					}
					self.timer = setTimeout( function (){
						self.validate();
					}, timeout );
				} else {
					self.validate();
				}
			}
		} );


		if ( self.hasRules( '=#' ) ) {
			var self_v_ = _v_().rules( self.config.rules ),
				depended_id = self_v_.parsedRules['=#'],
				el = document.getElementById( depended_id );

			if ( el ) {
				self.on( 'input keyup', function ( e ){
					self.validate( {
						highlight: !!self.value
					} );
				}, el )
			}
		}

		// blur validation without timeout
		if ( !~validate_event.indexOf( 'blur' ) && validate_event !== 'change' ) {
			self.on( 'blur', function (){
				if ( !self.get( 'rules' ) ) {
					return;
				}
				if ( self.timer ) {
					clearTimeout( self.timer );
				}
				if ( !(self.valid && self.hasRules( 'request' ) ) ) {
					self.validate( {
						no_timeout: true
					} );
				}
			} );
		}

		self.validate( { silence: true } );

		self.inited = true;
		return self;
	};
	tField_p.setRules = function ( rules, options ){
		var self = this;
		self.set( {
			rules: rules
		}, options );
		self.validate( options );
		return self;
	};

	tField_p.hasRules = function ( rules ){
		return _v_().rules( this.get( 'rules' ) ).hasRule( rules );
	};

	tField_p.addRules = function ( rules, options ){
		var self = this;
		return self.setRules( _v_().rules( self.get( 'rules' ) ).addRule( rules ).rule, options );
	};

	tField_p.delRule = function ( rules, options ){
		var self = this;
		return self.setRules( _v_().rules( self.get( 'rules' ) ).delRule( rules ).rule, options );
	};

	/**
	 * Highlight field with errorClass
	 * @param valid
	 * @returns {*}
	 */
	tField_p.error = function ( valid ){
		var self = this;
		self.valid = (!valid) ? true : false;

		if ( !self.highlight && !self.valid ) {
			return this;
		}

		var field = self.el,
			errorClass = self.get( 'errorClass' );

		if ( self.valid ) {
			self.removeClass( errorClass );
			self.data( 'error', null );
		} else {
			self.addClass( errorClass );
			self.data( 'error', '1' );
		}
		return self;
	};

	/**
	 * Hold field for a while
	 * @param {!boolean} hold
	 * @returns {*}
	 */
	tField_p.hold = function ( hold ){
		var self = this,
			is_holded = self.data( 'holded' );

		if ( hold === undefined ) {
			self.holded = !self.holded;
		} else {
			self.holded = (hold === true) ? true : false
		}
		self.data( 'holded', (self.holded ? 1 : null) );
		if ( !is_holded && self.holded === true ) {
			self.parent.holded++;
		} else if ( is_holded && self.holded === false ) {
			self.parent.holded--;
		}
		self.parent.submitControl();
		return self;
	};

	/**
	 * Here is the main field validation process
	 * @param {object} options
	 * @returns {boolean}
	 */
	tField_p.validate = function ( options ){
		var self = this,
			field = self.el,
			type = self.attr( 'type' ),
			result = true,
			own = self.get( 'own' ),
			request = self.get( 'request' ),
			rules = self.get( 'rules' ),
			_v_check = _v_( self.el.value || '' ).rules( rules ),
			is_checkbox = type == 'checkbox',
			is_required = _v_check.hasRule( '*' );

		if ( !rules ) {
			return result;
		}

		options = options || {};
		self.silence = (options.silence === true) ? true : false;

		self.highlight = (options.highlight === false || options.silence === true) ? false : true;
		self.fire_event = (options.fire_event === false || options.silence === true) ? false : true;

		if ( !self.silence ) {
			self.execute( 'before' );
			self.trigger( self.get( 'eventBefore' ) );
			self.trigger( self.validationStart );
		}

		if ( typeof own == 'function' ) {
			result = own.call( self.el );
		} else {
			if ( is_checkbox ) {
				var is_checked = field.checked;
				if ( !is_checked && is_required ) {
					result = false;
				}
			} else {
				// check request validation
				if ( is_required || self.el.value.length > 0 ) {
					result = _v_check.validate();
				}

				if ( request && result ) {
					self.requestValidate( options );
					self.highlight = true;
					return null;
				}
			}
		}

		if ( result ) {
			self.__validationSuccess();
		} else {
			self.__validationError();
		}

		self.highlight = true;
		self.fire_event = true;
		self.silence = false;
		return result;
	};

	tField_p.__validationStart = function (){

	};

	tField_p.__validationError = function (){
		var self = this;
		self.trigger( self.get( 'eventError' ) );
		self.parent.invalid += (self.valid) ? 1 : 0;
		self.parent.submitControl();
		self.error( true );
		self.execute( 'onerror' );
	};
	tField_p.__validationSuccess = function (){
		var self = this;
		self.trigger( self.get( 'eventValid' ) );
		self.parent.invalid -= (!self.valid) ? 1 : 0;
		self.parent.submitControl();
		self.error( false );
		self.execute( 'onvalid' );
	};


	tField_p.requestValidate = function ( options ){

		var self = this,
			name = self.attr( 'name' ),
			value = self.el.value,
			request = self.get( 'request' ),
			check_btn = self.parent.button( __getAttr( self.el, 'name' ) ),
			method = request.method.toLowerCase() == 'post' ? 'POST' : 'GET',
			data = request.data || {},
			success = request.success,
			url = (function (){
				var url = request.url || window.location.href;
				if ( method == 'GET' ) {
					url += (~url.indexOf( '?' )) ? '&' : '?';
					url += name + '=' + value;
				} else {
					data[name] = value;
				}
				return url;
			})(),

			timeout = options.no_timeout ? 0 : (request.timeout || 0),

			readyStateChange = function (){
				var xhr = self.xhr;

				if ( xhr.readyState == 1 ) {
					self.execute( request.start, [xhr] );
				}

				if ( xhr.readyState == 4 ) {
					// TODO: handle other errors (maybe with switch);
					if ( xhr.status == 200 ) {
						var result = (self.execute( request.end, [xhr.response] ) === true) ? true : false;
						self['__validation' + (result ? 'Success' : 'Error')]();
					}
				}
				if ( xhr.readyState == 4 || xhr.readystate == 0 ) {
					self.processing( false ).hold( false );
					if ( check_btn ) {
						check_btn.processing( false );
					}
				}
			},

			makeRequest = function (){
				self.processing( true ).hold( true );
				if ( check_btn ) {
					check_btn.processing( true );
				}
				var xhr = HTTP.newRequest();
				self.xhr = xhr;
				xhr.onreadystatechange = readyStateChange;
				xhr.open( method, url, true );
				xhr.setRequestHeader( "Accept-Language", "en" );
				if ( method == 'POST' ) {
					xhr.setRequestHeader( "Content-type", "application/x-www-form-urlencoded; charset=UTF-8" );
					xhr.send( __serialize( data ) );
				} else {
					xhr.send( null );
				}
			};

		if ( self.xhr && (self.xhr.readyState !== 0 || self.xhr.readyState !== 4) ) {
			self.xhr.abort();
			self.processing( false ).hold( false );
			if ( check_btn ) {
				check_btn.processing( false );
			}
		}

		if ( self.xhrTimeout ) {
			clearTimeout( self.xhrTimeout );
		}
		self.hold( false );

		if ( timeout > 0 && options.no_timeout !== true ) {
			self.xhrTimeout = setTimeout( function (){
				makeRequest();
			}, timeout );
		} else {
			makeRequest();
		}
	};

	tField_p.execute = function ( func, params ){
		var self = this;
		if ( self.silence ) {
			return;
		}

		if ( typeof func == 'string' ) {
			func = self.get( func );
		}
		return self.parent.execute( self.el, func, params );
	};


	/*
	 * Test tButton created by Element constructor
	 */
	var tButton = function ( parent, el ){
		var self = this,
			type = self.attr( 'type' ),
			name;

		self.parent = parent;
		self.el = el;
		self.config = {};

		self.events = [];

		name = type === 'submit' ? type : self.data( 'check' );
		self.name = name;
		self.set( __extend( __clone( parent.config ), __clone( (parent.config.buttons ? parent.config.buttons[name] : {}) ) ) );

		return self.init();
	};
	__extend_proto( tButton, Element );
	var tButton_p = tButton.prototype;

	tButton_p.init = function (){
		var self = this,
			el = self.el,
			parent = self.parent;

		if ( self.inited ) {
			self.destroy();
		}
		if ( el.type == 'submit' || el == parent.get( 'submitButton' ) ) {
			self.on( 'click', function ( e ){
				__prevent( e );
				parent.form.onsubmit( e );
				return false;
			} );
		} else {
			self.on( 'click', function (){
				var field = parent.field( self.data( 'check' ) );
				if ( field ) {
					field.validate( {no_timeout: true} );
				}
			} );
		}
		this.inited = true;
		return this;
	};

	tButton_p.disable = function (){
		var self = this;
		self.addClass( self.get( 'disabledClass' ) );
		return self;
	};

	tButton_p.enable = function (){
		var self = this;
		self.removeClass( self.get( 'disabledClass' ) );
		return self;
	};


	/*
	 * Helpers
	 * ----- ----- ----- ----- ----- ----- ----- ----- ----- -----
	 */
	/**
	 * Object extend
	 * @param object
	 * @param new_obj
	 * @returns {object}
	 */
	var __extend = function ( object, new_obj ){
		for ( var key in new_obj ) {
			if ( new_obj.hasOwnProperty( key ) ) {
				object[key] = new_obj[key];
			}
		}
		return object;
	};

	/**
	 * Method for preventing default events
	 * @param e
	 * @private
	 */
	var __prevent = function ( e ){
		if ( e.preventDefault ) {
			e.preventDefault();
		} else {
			event.returnValue = false;
		}
	};

	/**
	 * Object clone
	 * @returns {*|array|string}
	 */
	var __clone = function ( obj ){
		return __isArray( obj ) ? obj.slice() : __extend( {}, obj );
	};


	/**
	 * is array function
	 * @returns {boolean}
	 */
	var __isObject = function ( obj ){
		return toString.call( obj ) == '[object Object]';
	};


	var __isForm = function ( obj ){
		return obj && obj.nodeName === 'FORM';
	};

	/**
	 * is array function
	 * @returns {boolean}
	 */
	var __isArray = function ( obj ){
		return Object.prototype.toString.call( obj ) == '[object Array]';
	};


	/**
	 * detects is defined array has some value
	 * @param array
	 * @param value
	 * @returns {number}
	 * @private
	 */
	var __inArray = function ( array, value ){
		var index = -1,
			length = array ? array.length : 0;
		while ( ++index < length ) {
			if ( array[index] === value ) {
				return index;
			}
		}
		return -1;
	};

	var __clear = function ( arr ){
		if ( __isArray( arr ) ) {
			var new_arr = [];
			for ( var i = 0, a_l = arr.length; i < a_l; i++ ) {
				if ( arr[i] !== '' ) {
					new_arr.push( arr[i] );
				}
			}
			return new_arr;
		}
	};


	/**
	 * Work with data-attributes
	 *
	 * @param {object} element - element
	 * @param {string} attr - data-attribute
	 * @param {!string|number} value - new attribute value. [null - delete attr, undefined - return attr value, defined - set new value]
	 * @returns {*}
	 */
	var __data = function ( element, attr, value ){
		switch ( value ) {
			case null:
				__delAttr( element, 'data-' + attr );
				break;
			case undefined:
				return __getAttr( element, 'data-' + attr );
				break;
			default:
				__setAttr( element, 'data-' + attr, value );
				break;
		}
	};

	var __getAttr = function ( el, attr ){
		if ( !el ) {
			return null;
		}
		return el.getAttribute( attr );
	};
	var __setAttr = function ( el, attr, value ){
		if ( !el ) {
			return null;
		}
		el.setAttribute( attr, value );
	};
	var __delAttr = function ( el, attr ){
		if ( !el ) {
			return null;
		}
		el.removeAttribute( attr );
	};


	// AJAX Request functionality
	var HTTP = {};
	HTTP._factories = [
		function (){
			return new XMLHttpRequest();
		},
		function (){
			return new ActiveXObject( "Msxml2.XMLHTTP" );
		},
		function (){
			return new ActiveXObject( "Microsoft.XMLHTTP" );
		}
	];
	HTTP._factory = null;
	HTTP.newRequest = function (){
		if ( HTTP._factory !== null ) {
			return HTTP._factory();
		}
		for ( var i = 0; i < HTTP._factories.length; i++ ) {
			try {
				var factory = HTTP._factories[i];
				var request = factory();
				if ( request !== null ) {
					HTTP._factory = factory;
					return request;
				}
			} catch ( e ) {
				continue;
			}
		}
		HTTP._factory = function (){
			throw new Error( 'Object XMLHttpRequest not supported' );
		};
		HTTP._factory();
	};


	/**
	 * Serrialize data for request
	 * @param {object} data - object to serialize
	 * @returns {string}
	 */
	var __serialize = function ( data ){
		var pairs = [];
		var regexp = /%20/g;
		for ( var name in data ) {
			var pair = encodeURIComponent( name ).replace( regexp, '+' ) + '=';
			pair += encodeURIComponent( data[name].toString() ).replace( regexp, '+' );
			pairs.push( pair );
		}
		return pairs.join( '&' );
	};

})( window, document );
/**
 * _v_.js - validator
 * http://github.com/TjRus/_v_.js
 * (c) 2013 Vasiliy Zubach (aka TjRus) - http://tjrus.com/
 * _v_ may be freely distributed under the MIT license.
 */
"use strict";

(function ( window, document, undefined ){

	/**
	 * Validator constructor
	 *
	 * @constructor
	 * @param {!string} value
	 * @returns {*}
	 * @private
	 */
	var _v_ = function ( value ){
		if ( !(this instanceof _v_) ) {
			return new _v_( value );
		}
		this.value = value || '';
		this.separator = ' ';
		this.rule = '';

		this.parsedRules = [];

		return this;
	};
	var _v_proto = _v_.prototype;

	/**
	 * sets rules separator
	 * @param separator
	 * @returns {*}
	 */
	_v_proto.separate = function ( separator ){
		this.separator = separator;
		this.rules( this.rule ); // pars rules again
		return this;
	};

	/**
	 * set rules to validator
	 * @param rules
	 * @returns {*}
	 */
	_v_proto.rules = function ( rules ){
		this.rule = rules || '';
		this.parseRules();
		return this;
	};

	/**
	 * add rules to existed rules str
	 * @param rule
	 * @returns {*}
	 */
	_v_proto.addRule = function ( rule ){
		var parsed = this.parsedRules;
		var rule_v_ = _v_().rules( rule ).parsedRules;

		for ( var key in rule_v_ ) {
			var rule = rule_v_[key];
			if ( !rule ) {
				parsed[key] = rule;
				continue;
			}
			// has params and is in defined rules
			if ( rule && parsed.hasOwnProperty( key ) ) {
				// params is array
				parsed[key] = __toArray( parsed[key] );
				rule = __toArray( rule );
				for ( var i = 0, r_v_l = rule.length; i < r_v_l; i++ ) {
					if ( __inArray( parsed[key], rule[i] ) !== -1 ) {
						continue;
					}
					parsed[key].push( rule[i] );
				}
			} else {
				parsed[key] = rule;
			}
		}

		this.rule = __rulesStr( this.parsedRules, this.separator );
		return this;
	};

	/**
	 * Delete rule from validator rules
	 * @param rule
	 * @returns {*}
	 */
	_v_proto.delRule = function ( rule ){
		var parsed = this.parsedRules;
		var rule_v_ = _v_().rules( rule ).parsedRules;
		for ( var key in rule_v_ ) {
			var rules = rule_v_[key];
			if (!parsed.hasOwnProperty(key)) {
				continue;
			}
			if ( rules === undefined) {
				delete parsed[key];
				continue;
			}

			parsed[key] = __toArray( parsed[key] );
			rules = __toArray( rules );
			for ( var i = 0, r_l = rules.length; i < r_l; i++ ) {
				var index = __inArray( parsed[key], rules[i] );
				if ( ~index ) {
					parsed[key].splice( index, 1 );
				}
			}
			if ( parsed[key].length === 0 ) {
				delete parsed[key];
			} else if ( parsed[key].length === 1 ) {
				parsed[key] = parsed[key][0];
			}
		}
		this.rule = __rulesStr( this.parsedRules, this.separator );
		return this;
	};

	/**
	 * check has validator such rule or not
	 * @param rules
	 * @returns {boolean}
	 */
	_v_proto.hasRule = function ( rules ){
		var rule_v_ = _v_().rules( rules ).parsedRules;
		var parsed = this.parsedRules;
		for ( var key in rule_v_ ) {
			var rule = rule_v_[key];
			if ( !rule ) {
				if ( !parsed.hasOwnProperty( key ) ) {
					return false;
				} else {
					continue;
				}
			}
			parsed[key] = __toArray( parsed[key] );
			rule = __toArray( rule );
			for ( var i = 0, r_l = rule.length; i < r_l; i++ ) {
				if ( __inArray( parsed[key], rule[i] ) === -1 ) {
					return false;
				}
			}
		}
		return true;
	};


	/**
	 * parse validator rules and return object with rule keys and their values
	 * @returns {{}}
	 */
	_v_proto.parseRules = function (){
		var rules = this.rule.split( this.separator );
		var parsed = {};
		var keys = this.keys;

		for ( var i = 0, rules_length = rules.length; i < rules_length; i++ ) {
			var option = rules[i], func, rule, params = undefined;
			if ( keys[ option ] ) {
				func = keys[ option ];
				rule = option;
			} else {
				var keys_order = this.keys_order;
				for ( var j = 0, koLength = keys_order.length; j < koLength; j++ ) {
					var key = keys_order[j];
					if ( option.indexOf( key ) === 0 ) {
						rule = key;
						params = option.replace( key, '' );
						var paramArray = (/^\[(.+)\]$/).exec( params );
						if ( paramArray ) params = paramArray[1].split( ',' ); // TODO: params separator
						func = this.keys[rule];
						break;
					}
				}
			}
			if ( func ) {
				parsed[rule] = params;
			}
		}
		this.parsedRules = parsed;
		this.rule = __rulesStr( parsed, this.separator );
		return parsed;
	};

	/**
	 * main validate function.
	 * @param rules
	 * @returns {boolean}
	 */
	_v_proto.validate = function ( rules ){
		if ( rules ) {
			this.rules( rules );
		}
		var parsed = this.parsedRules;
		for ( var rule in parsed ) {
			try {
			if ( !this.keys[rule].call( this, parsed[rule] ) ) {
				return false;
			}
		} catch (e) {
			return false;
		}
		}
		return true;
	};


	/**
	 * method for extending _v_ with new rules and validate functions
	 * @param rule
	 * @param func
	 */
	_v_proto.extend = function ( rule, func ){
		_v_proto.keys[rule] = func
		_v_proto.keys_order.push( rule );

		_v_proto.keys_order = this.keys_order.sort( function ( a, b ){
			return b.length - a.length;
		} );
		return this;
	};

	_v_proto.keys = {}; // object with rules
	_v_proto.keys_order = []; // array with ordered rules

	window._v_ = _v_;


	/**
	 * Rules extends
	 * ---------- ---------- ---------- ---------- ----------
	 */
	var rules = {

		// required
		'*'   : function (){
			return this.value.length !== 0;
		},

		// alpha
		'a'   : function (){
			return (/^[a-z]+$/i).test( this.value );
		},

		// alpha numeric
		'a1'  : function (){
			return (/^[a-z0-9]+$/i).test( this.value );
		},

		// alpha dash
		'a_'  : function (){
			return (/^[a-z_-]+$/i).test( this.value );
		},

		// alpha numeric dash
		'a1_' : function (){
			return (/^[a-z0-9_-]+$/i).test( this.value );
		},

		// email
		'@'   : function (){
			var mail_regexp = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
			return mail_regexp.test( this.value );
		},

		// emails
		'@s'  : function (){
			var emails = this.value.split( ',' );
			for ( var i = 0; i < emails.length; i++ ) {
				if ( !_v_( emails[i] ).validate( '@' ) ) {
					return false;
				}
			}
			return true;
		},

		// ip address
		'ip'  : function (){
			return (/^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})$/i).test( this.value );
		},

		// base65 string
		'b64' : function (){
			return (/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{4})$/).test( this.value );
		},

		// URL
		'url' : function (){
			return (/^(https?|s?ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i).test( this.value );
		},

		// integer
		'int' : function (){
			return (/^\-?[0-9]+$/).test( this.value );
		},

		// Numeric
		'num' : function (){
			return (/^[0-9]+$/).test( this.value );
		},

		// Decimal
		'dec' : function (){
			return (/^\-?[0-9]*\.?[0-9]+$/).test( this.value );
		},

		// Natural
		'nat' : function (){
			return (/^[0-9]+$/i).test( this.value );
		},

		// length equals to
		'l='  : function ( length ){
			if ( typeof(length) == 'number' || typeof(length) == 'string' ) {
				return this.value.length == length;
			} else {
				for ( var key in length ) {
					if ( this.value.length == length[key] ) return true;
				}
			}
			return false;
		},

		// length more than
		'l>'  : function ( length ){
			return this.value.length > length;
		},

		// length more or equals to
		'l>=' : function ( length ){
			return this.value.length >= length;
		},

		// length less than
		'l<'  : function ( length ){
			return this.value.length < length;
		},

		// length less or equals to
		'l<=' : function ( length ){
			return this.value.length <= length;
		},

		// length is in range
		'lr=' : function ( range ){
			return (this.value.length >= range[0] && this.value.length <= range[1]);
		},

		// is greater than
		'>'   : function ( value ){
			var test = this.value;
			return (_v_( test ).validate( 'dec' ) && parseFloat( test ) > parseFloat( value ));
		},

		// is greater or equals to
		'>='  : function ( value ){
			var test = this.value;
			return (_v_( test ).validate( 'dec' ) && parseFloat( test ) >= parseFloat( value ));
		},

		// is less than
		'<'   : function ( value ){
			var test = this.value;
			return (_v_( test ).validate( 'dec' ) && parseFloat( test ) < parseFloat( value ));
		},

		// is less or equals to
		'<='  : function ( value ){
			var test = this.value;
			return (_v_( test ).validate( 'dec' ) && parseFloat( test ) <= parseFloat( value ));
		},

		// is in range
		'r='  : function ( value ){
			var test = this.value;
			return (_v_( test ).validate( 'dec' ) && parseFloat( test ) >= parseFloat( value[0] ) && parseFloat( test ) <= parseFloat( value[1] ));
		},

		// regular expression validation
		'reg=': function ( regExpression ){
			var reg = new RegExp( regExpression, 'i' );
			return reg.test( this.value );
		},

		// matches to
		'='   : function ( value ){
			var test = this.value;
			if ( typeof value == 'string' || typeof value == 'number' ) {
				return test == value;
			} else {
				for ( var i = 0, valueLength = value.length; i < valueLength; i++ ) {
					if ( value[i].indexOf( '#' ) === 0 ) {
						var el = __getElById( value[i].replace( '#', '' ) );
						if ( !el ) {
							return false;
						}
						if ( test == el.value ) return true;
					} else {
						if ( test == value[i] ) return true;
					}
				}
			}
			return false;
		},

		// matches to id
		'=#'  : function ( value ){
			var test = this.value;
			if ( typeof value == 'string' || typeof value == 'number' ) {
				value = value.split( ' ' );
				var el = __getElById( value );
				if ( !el ) {
					return false;
				}
				return test == el.value;
			} else {
				for ( var i = 0; i < value.length; i++ ) {
					var el = __getElById( value[i] );
					if ( el && test == el.value ) {
						return true;
					}
				}
			}
			return false;
		},

		// not matches to
		'!='  : function ( value ){
			value = (__isArray( value )) ? '[' + value.join( ',' ) + ']' : value; // TODO: value separator
			return !(_v_( this.value ).validate( '=' + value ));
		},

		// not contain
		'!'   : function ( value ){
			if ( typeof value == 'string' || typeof value == 'number' ) {
				return (this.value).toString().indexOf( value.toString() ) === -1;
			} else {
				for ( var key in value ) {
					if ( ~(this.value).toString().indexOf( value[key].toString() ) ) {
						return false;
					}
				}
			}
			return true;
		},

		// credit card (all cards type)
		'c'   : function ( value ){
			return (/^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/).test( this.value );
		},

		// visa card
		'cv'  : function ( value ){
			return (/^4[0-9]{12}(?:[0-9]{3})?$/).test( this.value );
		},

		// master card
		'cm'  : function ( value ){
			return (/^5[1-5][0-9]{14}$/).test( this.value );
		},

		// american express card
		'ca'  : function ( value ){
			return (/^3[47][0-9]{13}$/).test( this.value );
		},

		// discover card
		'cd'  : function ( value ){
			return (/^6(?:011|5[0-9]{2})[0-9]{12}$/).test( this.value );
		},

		// date format validation
		'D='  : function ( format ){
			return !!(__toDate( this.value, format ));
		}
	};

	// add extends
	for ( var key in rules ) {
		_v_().extend( key, rules[key] );
	}


	/**
	 * Helpers
	 * ---------- ---------- ---------- ---------- ----------
	 */

	// Date functions
	var __dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
	var __shortDayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
	var __dayChars = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];

	// Full, short and single character names for the months.  Override these to provide multi-language support.
	var __monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
	var __shortMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
	var __monthChars = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];

	var __daysInMonth = function ( month, year ){
		// If February, check for leap year
		if ( (month == 1) && (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0)) ) {
			return 29;
		}
		else {
			var days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
			return days[month];
		}
	};

	/**
	 * Attempts to convert a string into a date based on a given format.  Fields will
	 * match either the long or short form, except in the case of the year, where the
	 * string must match either a 2-digit or 4-digit format.  Ranges are checked.  Day
	 * names are expected if they are included in the format string, but are otherwise
	 * ignored.  Use ^ to force the use of a literal character. In other words, to
	 * have the character Y appear instead of the actual year, use ^Y.
	 *
	 * Field        | Full Form           | Short Form
	 * -------------+---------------------+-------------------
	 * Year         | Y (4 digits)        | y (2 digits)
	 * Month        | M (1 or 2 digits)   | m (1 or 2 digits)
	 * Month Name   | N (full name)       | n (abbr)
	 * Day of Month | D (1 or 2 digits)   | d (1 or 2 digits)
	 * Day Name     | W (full name)       | w (abbr)
	 * Hour (1-12)  | H (1 or 2 digits)   | h (1 or 2 digits)
	 * Hour (0-23)  | R (1 or 2 digits)   | r (1 or 2 digits)
	 * Minute       | I (1 or 2 digits)   | i (1 or 2 digits)
	 * Second       | S (1 or 2 digits)   | s (1 or 2 digits)
	 * AM/PM        | A (upper case)      | a (lower case)
	 *
	 * @param {Date} date Date that we should transform.
	 * @param {string} format The basic format of the string.
	 * @return {Date} The string as a date object.
	 */
	var __toDate = function ( date, format ){
		// Default values set to midnight Jan 1 of the current year.
		var year = new Date().getFullYear(),
			month = 0,
			day = 1,
			hours = 0,
			minutes = 0,
			seconds = 0;

		// Positions of each date element within the source string.  Use to know
		// which backreference to check after a successful match.
		var yearPos = -1,
			monthPos = -1,
			dayPos = -1,
			hoursPos = -1,
			minutesPos = -1,
			secondsPos = -1,
			amPmPos = -1;

		var monthStyle = 'm',       // How we interpret the month, digits (M/m) or names (N/n)
			hoursStyle = 'h';       // How we interpret the hours, 12-hour (h) or 24-hour (r)

		var position = 1,           // Position of the current date element (year, month, day, etc.) in the source string
			pattern = '';           // Date pattern to be matched.

		// Remove extraneous whitespace from source string and format string.
		var str = date.replace( /\s+/g, ' ' );
		format = format.replace( /\s+/g, ' ' );

		// Loop throught the format string, and build the regex pattern
		// for extracting the date elements.
		for ( var i = 0, len = format.length; i < len; i++ ) {
			var c = format.charAt( i );
			switch ( c ) {
				case 'Y' :
					pattern += '(\\d{4})';
					yearPos = position++;
					break;
				case 'y' :
					pattern += '(\\d{2})';
					yearPos = position++;
					break;
				case 'M' :
				case 'm' :
					pattern += '(\\d{1,2})';
					monthPos = position++;
					monthStyle = 'm';
					break;
				case 'N' :
					pattern += '(' + __monthNames.join( '|' ) + ')';
					monthPos = position++;
					monthStyle = 'N';
					break;
				case 'n' :
					pattern += '(' + __shortMonthNames.join( '|' ) + ')';
					monthPos = position++;
					monthStyle = 'n';
					break;
				case 'D' :
				case 'd' :
					pattern += '(\\d{1,2})';
					dayPos = position++;
					break;
				case 'W' : // We'll match W, but won't do anything with it.
					pattern += '(' + __dayNames.join( '|' ) + ')';
					position++;
					break;
				case 'w' : // We'll match w, but won't do anything with it.
					pattern += '(' + __shortDayNames.join( '|' ) + ')';
					position++;
					break;
				case 'H' :
				case 'h' :
					pattern += '(\\d{1,2})';
					hoursPos = position++;
					hoursStyle = 'h';
					break;
				case 'R' :
				case 'r' :
					pattern += '(\\d{1,2})';
					hoursPos = position++;
					hoursStyle = 'r';
					break;
				case 'I' :
				case 'i' :
					pattern += '(\\d{1,2})';
					minutesPos = position++;
					break;
				case 'S' :
				case 's' :
					pattern += '(\\d{1,2})';
					secondsPos = position++;
					break;
				case 'A' :
				case 'a' :
					pattern += '(AM|am|PM|pm)';
					amPmPos = position++;
					break;
				default :
					pattern += (c == '^' ? format.charAt( ++i ) : c);
			}
		}

		// Pull out the date elements from the input string
		var matches = str.match( new RegExp( pattern ) );
		if ( !matches ) {
			return null;
		}

		// Now we have to interpret each of those parts...

		if ( yearPos > -1 ) {
			year = parseInt( matches[yearPos], 10 );
			year = (year < 50 ? year + 2000 : (year < 100 ? year + 1900 : year));
		}

		if ( monthPos > -1 ) {
			switch ( monthStyle ) {
				case 'm':
					month = parseInt( matches[monthPos], 10 ) - 1;    // JavaScript months are zero based, user input generally is not.
					if ( month > 11 )
						return null;
					break;
				case 'N':
					month = parseInt( __monthNumbers[matches[monthPos]], 10 );
					if ( isNaN( month ) )
						return null;
					break;
				case 'n':
					month = parseInt( __shortMonthNumbers[matches[monthPos]], 10 );
					if ( isNaN( month ) )
						return null;
					break;
			}
		}

		if ( dayPos > -1 ) {
			day = parseInt( matches[dayPos], 10 );
			if ( (day < 1) || (day > __daysInMonth( month, year )) )
				return null;
		}

		if ( hoursPos > -1 ) {
			hours = parseInt( matches[hoursPos], 10 );
			if ( hoursStyle == 'h' && (hours === 0 || hours > 12) )
				return null;
			else if ( hours > 23 )
				return null;
		}

		if ( minutesPos > -1 ) {
			minutes = parseInt( matches[minutesPos], 10 );
			if ( minutes > 59 )
				return null;
		}

		if ( secondsPos > -1 ) {
			seconds = parseInt( matches[secondsPos], 10 );
			if ( seconds > 59 )
				return null;
		}

		// Convert 12-hour time, if used, to 24-hour time.
		if ( amPmPos > -1 ) {
			var amPm = matches[amPmPos];
			if ( (amPm == 'pm' || amPm == 'PM') && (hours < 12) )
				hours += 12;
		}

		return new Date( year, month, day, hours, minutes, seconds );
	};


	/**
	 * Simple getElementByID function
	 * @param {string} id - id of HTML element
	 * @returns {HTMLElement}
	 * @private
	 */
	var __getElById = function ( id ){
		return document.getElementById( id );
	}

	/**
	 * Check is obj is array or not
	 * @param obj - element we should check
	 * @returns {boolean}
	 * @private
	 */
	var __isArray = function ( obj ){
		return Object.prototype.toString.call( obj ) == '[object Array]';
	};

	/**
	 * detects is defined array has some value
	 * @param array
	 * @param value
	 * @returns {number}
	 * @private
	 */
	var __inArray = function ( array, value ){
		var index = -1,
			length = array ? array.length : 0;
		while ( ++index < length ) {
			if ( array[index] === value ) {
				return index;
			}
		}
		return -1;
	};
	window.__inArray = __inArray;


	/**
	 * convert item to Array.
	 * @param el
	 * @returns {Array}
	 * @private
	 */
	var __toArray = function ( el ){
		return (!__isArray( el )) ? [el] : el;
	}

	/**
	 * convert rules object to string
	 * @param obj
	 * @param separator
	 * @returns {string}
	 * @private
	 */
	var __rulesStr = function ( obj, separator ){
		separator = separator || ' ';
		var str = separator;
		for ( var key in obj ) {
			var rules = obj[key];
			if ( __isArray( rules )){
				rules = __clearArray(rules);
			}
			if ( !rules ) {
				str += key + separator;
			} else if ( __isArray( rules ) ) {
				str += key + '[' + rules.join( ',' ) + ']' + separator; // TODO: rule values separator
			} else {
				str += key + rules + separator;
			}
		}
		str = str.substr( separator.length, str.length - separator.length * 2 );
		return str;
	};

	var __clearArray = function(arr){
		var new_arr = [];
		for (var i = 0, a_l = arr.length; i < a_l; i++) {
			if (arr[i]) {
				new_arr.push(arr[i]);
			}
		}
		return (new_arr && new_arr.length > 1) ? new_arr : new_arr[0];
	}

})( window, document );