/**
 * @author Vlad Yakovlev (scorpix@design.ru)
 * @copyright Art.Lebedev Studio (http://www.artlebedev.ru)
 * @requires jQuery
 */

/**
 * Валидатор "кривых" форм.
 *
 * @param {Object} forms Ассоциативный массив; индекс - тип формы, значение - объект jQuery формы.
 * @param {Object} fields Ассоциативный массив проверяемых элементов формы; индекс - имя элемента (атрибут <code>name</code>), значение - ассоциативный массив:
 *   <ul>
 *     <li><code>forms</code> - массив типов форм, в которых встречается элемент;</li>
 *     <li><code>validate...</code> - тип валидатора, который содержит опции для валидатора;
 *       постфиксом служит один из индексов <code>FormValidatorType</code>, а также <code>Required</code> - обязательное поле.</li>
 *   </ul>
 */
function SkewedValidator(forms, fields) {

	/**
	 * Формы валидации. Индекс - тип формы, значение - объект <code>FormValidator</code>.
	 * @type Object
	 */
	this.validators = {};

	/**
	 * Ассоциативный массив полей в формах для быстрого поиска этих полей.
	 * Индекс - тип формы, значение - ассоциативный массив с индексом - названием поля (атрибут <code>name</code>).
	 * @type Object
	 */
	this.fieldsInForms = {};

	// Заполняем this.validators.
	for (var formName in forms) {

		// Инициализируем объекты, чтоб потом к ним обращаться.
		this.fieldsInForms[formName] = {};

		var fieldsInForm = {};

		for (var fieldName in fields) {
			if ($.inArray(formName, fields[fieldName])) {
				fieldsInForm[fieldName] = fields[fieldName];
			}
		}

		this.validators[formName] = new FormValidator(this, {
			form: forms[formName],
			fields: fieldsInForm
		});
	}

	for (var fieldName in fields) {
		for (var i = 0; i < fields[fieldName]['forms'].length; i++) {
			this.fieldsInForms[fields[fieldName]['forms'][i]][fieldName] = true;
		}
	}
}

/**
 * Синхронизирует значения в одинаковых полях разных форм.
 * Вызывается из элемента формы, а функция меняет значения во всех аналогичных полях.
 *
 * @param {jQuery} element
 */
SkewedValidator.prototype.updateFields = function(element) {

	var name = element.attr('name');
	var value = element.val();

	for (var validatorName in this.fieldsInForms) {
		if (this.fieldsInForms[validatorName][name]) {
			this.validators[validatorName].updateFieldValue(name, value);
		}
	}
}


/**
 * Валидатор формы.
 *
 * @param {SkewedValidator} skewedValidator Объект, который инициализировал валидатор.
 * @param {Object} options Опции валидатора:
 * <ul>
 *   <li><code>form</code> - объект jQuery формы;</li>
 *   <li><code>fields</code> - ассоциативный массив проверяемых элементов формы; индекс - имя элемента (атрибут <code>name</code>), значение - ассоциативный массив:
 *     <ul>
 *       <li><code>validate...</code> - тип валидатора, который содержит <code>message</code> (сообщение, выдаваемое при ошибке), а также опции для валидатора;
 *         постфиксом служит один из индексов <code>FormValidatorType</code>, а также <code>Required</code> - обязательное поле.</li>
 *     </ul>
 *   </li>
 * </ul>
 */
function FormValidator(skewedValidator, options) {

	/**
	 * Корневой  объект.
	 * @type SkewedValidator
	 */
	this.skewedValidator = skewedValidator;

	/**
	 * Элемент формы.
	 * @type jQuery
	 */
	this.form = options.form;

	/**
	 * Ассоциативный массив объектов валидации элементов; индекс - название элемента формы, значение - объект валидации элемента.
	 * @type Object
	 */
	this.fields = {};

	/**
	 * Элемент сабмита.
	 * @type jQuery
	 */
	this.submit = this.form.find('input:image, input:submit');

	/**
	 * Флаг, который позволяет форме сабмититься.
	 * Нужно сабмитить форму, чтоб работало автозаполнение полей.
	 * @type Boolean
	 */
	this.maySubmit = false;

	if (!options.fields) {
		return;
	}

	// Заполняем массив валидации элементов.
	for (var fieldName in options.fields) {
		var elField = this.form.find("input[name='" + fieldName + "']");

		if (elField.size()) {
			this.fields[fieldName] = new FormValidatorElement(this, elField, options.fields[fieldName]);
		}
	}

	if (this.check()) {
		this.submit.attr('disabled', '').removeClass('disabled');
	}

	var me = this;

	this.form.submit(function() {
		return me.submitForm();
	});
}

FormValidator.prototype = {

	/**
	 * Проверяет элементы формы на корректный ввод.
	 *
	 * @return {Boolean} Успешность проверки.
	 */
	check: function() {

		// Все элементы проверяются валидаторами.
		for (var fieldName in this.fields) {
			this.fields[fieldName].check();
		}

		return this.isChecked();
	},

	/**
	 * Меняет состояние формы исходя из имеющихся в форме ошибок.
	 *
	 * @return {Boolean} Наличие ошибок в форме (true - ошибок нет, false - есть).
	 */
	isChecked: function() {
		var checked = true;

		// Элементы опрашиваются на факт наличия ошибок.
		for (var fieldName in this.fields) {
			if (!this.fields[fieldName].isChecked()) {
				checked = false;

				break;
			}
		}

		if (checked) {
			this.submit.attr('disabled', '').removeClass('disabled');
		} else {
			this.submit.attr('disabled', 'disabled').addClass('disabled');
		}

		return checked;
	},

	/**
	 * Обновляет значение у заданного элемента.
	 *
	 * @param {String} fieldName Название поля (атрибут <code>name</code>), у которого берется значение.
	 * @param {String} value Значение, которое нужно задать.
	 */
	updateFieldValue: function(fieldName, value) {
		this.fields[fieldName].updateValue(value);
	},

	submitForm: function() {

		// Ajax-запрос прошел - можно сабмитить.
		if (this.maySubmit) {
			this.form.attr('action', location.href);

			return true;
		}

		if (!this.isChecked()) {
			return false;
		}

		var formData = {};

		this.form.find('input, select, textarea').each(function () {
			formData[this.name] = this.value;
		});

		var me = this;

		$.ajax({
			url: this.form.attr('action'),
			type: 'POST',
			cache: false,
			dataType: 'xml',
			data: formData,
			success: function (data) {
				/**
				 * В случае успешной проверки:
				 * <root>
				 *   <status>success</status>
				 * </root>
				 * В случае ошибок:
				 * <root>
				 *   <status>error</status>
				 *   <error field="email">Ошибочный формат e-mail.</error>
				 *   <error field="password">Я не запомнил пароль. Еще раз.</error>
				 * </root>
				 */
				var xmlData = me.getXml(data);
				xmlData = $('result', xmlData);

				if ('success' == xmlData.find('status').text()) {
					form.attr('action', location.href);

					// Теперь форму можно сабмитить.
					maySubmit = true;
					this.submit();
				}
				else {
					xmlData.find('error').each(function() {
						var fieldName = $(this).attr('field');
						me.fields[fieldName].showError($(this).text());
					});
				}
			}
		});
/*
		// {{{ Debug
		var xmlData = me.getXml('<result><status>error</status><error field="email">Ошибочный формат e-mail.</error><error field="password">Я не запомнил пароль. Еще раз.</error></result>');
		//var xmlData = me.getXml('<root><status>success</status></root>');
		xmlData = $('result', xmlData);

		if ('success' == xmlData.find('status').text()) {
			// Теперь форму можно сабмитить.
			this.maySubmit = true;
			this.form.submit();
		}
		else {
			xmlData.find('error').each(function() {
				var fieldName = $(this).attr('field');
				me.fields[fieldName].showError($(this).text());
			});
		}
		// }}} Debug*/

		return false;
	},

	/**
	 * Возвращает XML.
	 * @param {String} text XML в строке.
	 * @return {DomElement} XML в объекте.
	 */
	getXml: function(text) {
		var xml = null;

		try {
			if (window.ActiveXObject) { // IE
				xml = new ActiveXObject('Microsoft.XMLDOM');
				xml.async = false;
				xml.loadXML(text);
			} else if (window.DOMParser) { // Все остальные
				var xml = (new DOMParser()).parseFromString(text, 'text/xml');
			}

			if (!xml || !xml.documentElement
				|| xml.documentElement.nodeName == 'parsererror'
				|| xml.getElementsByTagName('parsererror').length) {
				return false;
			}
		} catch (error) {
			return false;
		}

		return xml;
	}

};


/**
 * Валидатор элемента формы.
 *
 * @param {FormValidator} validator Валидатор формы.
 * @param {jQuery} element Элемент формы, который валидируется.
 * @param {Object} options Опции элемента формы:
 * <ul>
 *   <li><code>validate...</code> - тип валидатора, который содержит <code>message</code> (сообщение, выдаваемое при ошибке), а также опции для валидатора;
 *     постфиксом служит один из индексов <code>FormValidatorType</code>, а также <code>Required</code> - обязательное поле.</li>
 * </ul>
 */
function FormValidatorElement(validator, element, options) {

	/**
	 * Валидатор формы.
	 * @type FormValidator
	 */
	this.validator = validator;

	/**
	 * Элемент формы.
	 * @type jQuery
	 */
	this.element = element;

	/**
	 * Тип ошибки валидации (пусто - нет ошибок).
	 * @type String
	 */
	this.errorValidate = '';

	/**
	 * Элемент страницы, который показывает ошибку элемента формы.
	 * @type jQuery
	 */
	this.errorBlock = validator.form.find('.error_' + this.getClass(element.attr('name')));

	/**
	 * CSS класс для родителя элемента формы в состоянии ошибки.
	 * @type String
	 */
	this.errorClass = 'field_error';

	/**
	 * Ассоциативный массив валидаторов: индекс - тип валидатора, значение - опции валидатора.
	 * @type Object
	 */
	this.validates = {};

	/**
	 * Флаг первой проверки элемента.
	 * @type Boolean
	 */
	this.firstCheck = true;

	/**
	 * Обработчик, когда пришла ошибка с сервера и пользователь на нее нажал.
	 */
	this.errorHandler;

	/**
	 * Префикс опции валидатора.
	 * @type String
	 */
	var prefix = 'validate';

	// Заполняем массив валидаторов.
	for (var optionName in options) {
		if (0 == optionName.indexOf(prefix)) {
			var validateName = optionName.substr(prefix.length);

			if ('Required' == validateName || FormValidatorTypes[validateName]) {
				this.validates[validateName] = options[optionName];
			}
		}
	}

	var me = this;

	this.element.keyup(function() {
		me.check();
	}).change(function() {
		me.check();
	});
}

FormValidatorElement.prototype = {

	/**
	 * Проверяет элемент формы на ошибки.
	 */
	check: function() {
		var errorValidate = '';
		var elVal = this.element.val();

		for (var validateName in this.validates) {

			if ('Required' == validateName) {
			// Проверка на обязательное поле.
				if ('' == elVal) {
					errorValidate = 'Required';

					break;
				}

			} else if ('' != elVal && !FormValidatorTypes[validateName](this.element, this.validates[validateName])) {
			// Проверка других валидаторов.
				errorValidate = validateName;

				break;
			}
		}

		this.errorValidate = errorValidate;

		// Если проверка проходит в первый раз (при загрузке формы), обновлять состояние элемента не нужно.
		if (this.firstCheck) {
			this.firstCheck = false;
		} else {
			// Запускаем каскад операций по синхронизации значений в аналогичных полях других форм.
			this.validator.skewedValidator.updateFields(this.element);
			this.updateState();
		}

		this.validator.isChecked();
	},

	/**
	 * Возвращает состояние ошибки элемента формы (true - все чисто, false - ошибка).
	 * @return {Boolean}
	 */
	isChecked: function() {
		return '' == this.errorValidate;
	},

	/**
	 * Обновляет состояние элемента формы (показывает или отключает сообщение об ошибке).
	 */
	updateState: function() {

		if ('' == this.errorValidate) {

			if (this.errorClass) {
				this.element.parent().removeClass(this.errorClass);
			}

		} else {

			if (this.errorClass) {
				this.element.parent().addClass(this.errorClass);
			}

		}
	},

	/**
	 * Обновляет значение у элемента.
	 *
	 * @param {String} value Собственно значение.
	 */
	updateValue: function(value) {

		if (value == this.element.val()) {
			return;
		}

		this.element.val(value);
		this.element.change();
	},

	showError: function(message) {
		var fieldName = this.element.attr('name');

		this.validator.form.find('.field_' + this.getClass(fieldName)).addClass('hidden');
		this.errorBlock.find('.pseudo_link').text(message);
		this.errorBlock.removeClass('hidden');

		var me = this;

		if (!this.errorHandler) {
			this.errorHandler = function() {
				me.validator.form.find('.field_' + this.getClass(fieldName)).removeClass('hidden');
				me.errorBlock.find('.pseudo_link').text('');
				me.errorBlock.addClass('hidden');
				me.validator.form.find('.hidden_on_error').css('visibility', '');
			};
		}

		me.validator.form.find('.hidden_on_error').css('visibility', 'hidden');

		this.errorBlock.find('.pseudo_link').click(this.errorHandler);
	},

	getClass: function(fieldName) {
		return fieldName.replace('.', '_');
	}
};


/**
 * Функции валидации.
 * @type Object
 */
FormValidatorTypes = {

	/**
	 * Проверка e-mail.
	 *
	 * @param {jQuery} el Элемент, у которого производится проверка.
	 * @return {Boolean} true - нет ошибок, false - есть.
	 */
	Email: function(el) {
		var validRegExp = /^\s*[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\s*$/i;

		return -1 != el.val().search(validRegExp);
	},

	/**
	 * Проверка на минимальное количество символов.
	 *
	 * @param {jQuery} el Элемент, у которого производится проверка.
	 * @param {Object} options Опции валидатора:
	 * <ul>
	 *   <li><code>min</code> - минимальное количество символов.</li>
	 * </ul>
	 * @return {Boolean} true - нет ошибок, false - есть.
	 */
	MinLength: function(el, options) {
		var minLength = options && options.min ? options.min : 0;

		return 0 >= minLength ? true : (minLength <= el.val().length);
	}
};