/**
 * jQuery yiiactiveform plugin file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright 2008-2010 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 * @since 1.1.1
 */

(function ($) {
	/*
	 * returns the value of the CActiveForm input field
	 * performs additional checks to get proper values for checkbox / radiobutton / checkBoxList / radioButtonList
	 * @param o object the jQuery object of the input element
	 */
	var getAFValue = function (o) {
		var type,
			c = [];
		if (!o.length) {
			return undefined;
		}
		if (o[0].tagName.toLowerCase() === 'span') {
			o.find(':checked').each(function () {
				c.push(this.value);
			});
			return c.join(',');
		}
		type = o.attr('type');
		if (type === 'checkbox' || type === 'radio') {
			return o.filter(':checked').val();
		} else {
			return o.val();
		}
	};

	/**
	 * yiiactiveform set function.
	 * @param options map settings for the active form plugin. Please see {@link CActiveForm::options} for available options.
	 */
	$.fn.yiiactiveform = function (options) {
		return this.each(function () {
			var settings = $.extend({}, $.fn.yiiactiveform.defaults, options || {}),
				$form = $(this);

			if (settings.validationUrl === undefined) {
				settings.validationUrl = $form.attr('action');
			}
			$.each(settings.attributes, function (i) {
				this.value = getAFValue($form.find('#' + this.inputID));
				settings.attributes[i] = $.extend({}, {
					validationDelay: settings.validationDelay,
					validateOnChange: settings.validateOnChange,
					validateOnType: settings.validateOnType,
					hideErrorMessage: settings.hideErrorMessage,
					inputContainer: settings.inputContainer,
					errorCssClass: settings.errorCssClass,
					successCssClass: settings.successCssClass,
					beforeValidateAttribute: settings.beforeValidateAttribute,
					afterValidateAttribute: settings.afterValidateAttribute,
					validatingCssClass: settings.validatingCssClass,
					errorCallback: settings.errorCallback
				}, this);
			});
			$form.data('settings', settings);

			settings.submitting = false;  // whether it is waiting for ajax submission result
			var validate = function (attribute, forceValidate) {
				if (forceValidate) {
					attribute.status = 2;
				}
				$.each(settings.attributes, function () {
					if (this.value !== getAFValue($form.find('#' + this.inputID))) {
						this.status = 2;
						forceValidate = true;
					}
				});
				if (!forceValidate) {
					return;
				}

				if (settings.timer !== undefined) {
					clearTimeout(settings.timer);
				}
				settings.timer = setTimeout(function () {
					if (settings.submitting || $form.is(':hidden')) {
						return;
					}
					if (attribute.beforeValidateAttribute === undefined || attribute.beforeValidateAttribute($form, attribute)) {
						$.each(settings.attributes, function () {
							if (this.status === 2) {
								this.status = 3;
								$.fn.yiiactiveform.getInputContainer(this, $form).addClass(this.validatingCssClass);
							}
						});
						$.fn.yiiactiveform.validate($form, function (data) {
							var hasError = false;
							$.each(settings.attributes, function () {
								if (this.status === 2 || this.status === 3) {
									hasError = $.fn.yiiactiveform.updateInput(this, data, $form) || hasError;
								}
							});
							if (attribute.afterValidateAttribute !== undefined) {
								attribute.afterValidateAttribute($form, attribute, data, hasError);
							}
						},settings.errorCallback);
					}
				}, attribute.validationDelay);
			};

			$.each(settings.attributes, function (i, attribute) {
				if (this.validateOnChange) {
					$form.find('#' + this.inputID).on('change', function () {
						validate(attribute, false);
					}).on('blur', function () {
						if (attribute.status !== 2 && attribute.status !== 3) {
							validate(attribute, !attribute.status);
						}
					});
				}
				if (this.validateOnType) {
					$form.find('#' + this.inputID).keyup(function () {
						if (attribute.value !== getAFValue($(this))) {
							validate(attribute, false);
						}
					});
				}
			});

			if (settings.validateOnSubmit) {
				$form.on('mouseup keyup', ':submit', function () {
					$form.data('submitObject', $(this));
				});
				var validated = false;
				$form.on('submit', function () {
					if (validated) {
						validated = false;
						return true;
					}
					if (settings.timer !== undefined) {
						clearTimeout(settings.timer);
					}
					settings.submitting = true;
					if (settings.beforeValidate === undefined || settings.beforeValidate($form)) {
						$.fn.yiiactiveform.validate($form, function (data) {
							var hasError = false;
							$.each(settings.attributes, function () {
								hasError = $.fn.yiiactiveform.updateInput(this, data, $form) || hasError;
							});
							$.fn.yiiactiveform.updateSummary($form, data);
							if (settings.afterValidate === undefined || settings.afterValidate($form, data, hasError)) {
								if (!hasError) {
									validated = true;
									var $button = $form.data('submitObject') || $form.find(':submit:first');
									// TODO: if the submission is caused by "change" event, it will not work
									if ($button.length) {
										$button.trigger('click');
									} else {  // no submit button in the form
										$form.trigger('submit');
									}
									return;
								}
							}
							settings.submitting = false;
						},settings.errorCallback);
					} else {
						settings.submitting = false;
					}
					return false;
				});
			}

			/*
			 * In case of reseting the form we need to reset error messages
			 * NOTE1: $form.reset - does not exist
			 * NOTE2: $form.on('reset', ...) does not work
			 */
			$form.bind('reset', function () {
				/*
				 * because we bind directly to a form reset event, not to a reset button (that could or could not exist),
				 * when this function is executed form elements values have not been reset yet,
				 * because of that we use the setTimeout
				 */
				setTimeout(function () {
					$.each(settings.attributes, function () {
						this.status = 0;
						var $error = $form.find('#' + this.errorID),
							$container = $.fn.yiiactiveform.getInputContainer(this, $form);

						$container.removeClass(
							this.validatingCssClass + ' ' +
							this.errorCssClass + ' ' +
							this.successCssClass
						);

						$error.html('').hide();

						/*
						 * without the setTimeout() we would get here the current entered value before the reset instead of the reseted value
						 */
						this.value = getAFValue($form.find('#' + this.inputID));
					});
					/*
					 * If the form is submited (non ajax) with errors, labels and input gets the class 'error'
					 */
					$form.find('label, :input').each(function () {
						$(this).removeClass(settings.errorCss);
					});
					$('#' + settings.summaryID).hide().find('ul').html('');
					//.. set to initial focus on reset
					if (settings.focus !== undefined && !window.location.hash) {
						$form.find(settings.focus).trigger('focus');
					}
				}, 1);
			});

			/*
			 * set to initial focus
			 */
			if (settings.focus !== undefined && !window.location.hash) {
				$form.find(settings.focus).trigger('focus');
			}
		});
	};

	/**
	 * Returns the container element of the specified attribute.
	 * @param attribute object the configuration for a particular attribute.
	 * @param form the form jQuery object
	 * @return jQuery the jQuery representation of the container
	 */
	$.fn.yiiactiveform.getInputContainer = function (attribute, form) {
		if (attribute.inputContainer === undefined) {
			return form.find('#' + attribute.inputID).closest('div');
		} else {
			return form.find(attribute.inputContainer).filter(':has("#' + attribute.inputID + '")');
		}
	};

	/**
	 * updates the error message and the input container for a particular attribute.
	 * @param attribute object the configuration for a particular attribute.
	 * @param messages array the json data obtained from the ajax validation request
	 * @param form the form jQuery object
	 * @return boolean whether there is a validation error for the specified attribute
	 */
	$.fn.yiiactiveform.updateInput = function (attribute, messages, form) {
		attribute.status = 1;
		var $error, $container,
			hasError = false,
			$el = form.find('#' + attribute.inputID),
			errorCss = form.data('settings').errorCss;

		if ($el.length) {
			hasError = messages !== null && $.isArray(messages[attribute.id]) && messages[attribute.id].length > 0;
			$error = form.find('#' + attribute.errorID);
			$container = $.fn.yiiactiveform.getInputContainer(attribute, form);

			$container.removeClass(
				attribute.validatingCssClass + ' ' +
				attribute.errorCssClass + ' ' +
				attribute.successCssClass
			);
			$container.find('label, :input').each(function () {
				$(this).removeClass(errorCss);
			});

			if (hasError) {
				$error.html(messages[attribute.id][0]);
				$container.addClass(attribute.errorCssClass);
			} else if (attribute.enableAjaxValidation || attribute.clientValidation) {
				$container.addClass(attribute.successCssClass);
			}
			if (!attribute.hideErrorMessage) {
				$error.toggle(hasError);
			}

			attribute.value = getAFValue($el);
		}
		return hasError;
	};

	/**
	 * updates the error summary, if any.
	 * @param form jquery the jquery representation of the form
	 * @param messages array the json data obtained from the ajax validation request
	 */
	$.fn.yiiactiveform.updateSummary = function (form, messages) {
		var settings = $(form).data('settings'),
			content = '';
		if (settings.summaryID === undefined) {
			return;
		}
		if (messages) {
			var summaryAttributes = [];
			for (var i in settings.attributes) {
				if (settings.attributes[i].summary) {
					summaryAttributes.push(settings.attributes[i].id);
				}
			}
			$.each(settings.attributes, function () {
				if ($.inArray(this.id, summaryAttributes) !== -1 && $.isArray(messages[this.id])) {
					$.each(messages[this.id], function (j, message) {
						content = content + '<li>' + message + '</li>';
					});
				}
			});
		}
		$('#' + settings.summaryID).toggle(content !== '').find('ul').html(content);
	};

	/**
	 * Performs the ajax validation request.
	 * This method is invoked internally to trigger the ajax validation.
	 * @param form jquery the jquery representation of the form
	 * @param successCallback function the function to be invoked if the ajax request succeeds
	 * @param errorCallback function the function to be invoked if the ajax request fails
	 */
	$.fn.yiiactiveform.validate = function (form, successCallback, errorCallback) {
		var $form = $(form),
			settings = $form.data('settings'),
			needAjaxValidation = false,
			messages = {};
		$.each(settings.attributes, function () {
			var value,
				msg = [];
			if (this.clientValidation !== undefined && (settings.submitting || this.status === 2 || this.status === 3)) {
				value = getAFValue($form.find('#' + this.inputID));
				this.clientValidation(value, msg, this);
				if (msg.length) {
					messages[this.id] = msg;
				}
			}
			if (this.enableAjaxValidation && !msg.length && (settings.submitting || this.status === 2 || this.status === 3)) {
				needAjaxValidation = true;
			}
		});

		if (!needAjaxValidation || settings.submitting && !$.isEmptyObject(messages)) {
			if (settings.submitting) {
				// delay callback so that the form can be submitted without problem
				setTimeout(function () {
					successCallback(messages);
				}, 200);
			} else {
				successCallback(messages);
			}
			return;
		}

		var $button = $form.data('submitObject'),
			extData = '&' + settings.ajaxVar + '=' + $form.attr('id');
		if ($button && $button.length) {
			extData += '&' + $button.attr('name') + '=' + $button.attr('value');
		}

		$.ajax({
			url: settings.validationUrl,
			type: $form.attr('method'),
			data: $form.serialize() + extData,
			dataType: 'json',
			success: function (data) {
				if (data !== null && typeof data === 'object') {
					$.each(settings.attributes, function () {
						if (!this.enableAjaxValidation) {
							delete data[this.id];
						}
					});
					successCallback($.extend({}, messages, data));
				} else {
					successCallback(messages);
				}
			},
			error: function () {
				if (errorCallback !== undefined) {
					errorCallback();
				}
			}
		});
	};

	/**
	 * Returns the configuration for the specified form.
	 * The configuration contains all needed information to perform ajax-based validation.
	 * @param form jquery the jquery representation of the form
	 * @return object the configuration for the specified form.
	 */
	$.fn.yiiactiveform.getSettings = function (form) {
		return $(form).data('settings');
	};

	$.fn.yiiactiveform.defaults = {
		ajaxVar: 'ajax',
		validationUrl: undefined,
		validationDelay: 200,
		validateOnSubmit: false,
		validateOnChange: true,
		validateOnType: false,
		hideErrorMessage: false,
		inputContainer: undefined,
		errorCss: 'error',
		errorCssClass: 'error',
		successCssClass: 'success',
		validatingCssClass: 'validating',
		summaryID: undefined,
		timer: undefined,
		beforeValidateAttribute: undefined, // function (form, attribute) | boolean
		afterValidateAttribute: undefined,  // function (form, attribute, data, hasError)
		beforeValidate: undefined, // function (form) | boolean
		afterValidate: undefined,  // function (form, data, hasError) | boolean
		focus: undefined,  // jquery selector that indicates which element to receive input focus initially
		/**
		 * list of attributes to be validated. Each array element is of the following structure:
		 * {
		 *     id: 'ModelClass_attribute', // the unique attribute ID
		 *     model: 'ModelClass', // the model class name
		 *     name: 'name', // attribute name
		 *     inputID: 'input-tag-id',
		 *     errorID: 'error-tag-id',
		 *     value: undefined,
		 *     status: 0,  // 0: empty, not entered before,  1: validated, 2: pending validation, 3: validating
		 *     validationDelay: 200,
		 *     validateOnChange: true,
		 *     validateOnType: false,
		 *     hideErrorMessage: false,
		 *     inputContainer: undefined,
		 *     errorCssClass: 'error',
		 *     successCssClass: 'success',
		 *     validatingCssClass: 'validating',
		 *     enableAjaxValidation: true,
		 *     enableClientValidation: true,
		 *     clientValidation: undefined, // function (value, messages, attribute) | client-side validation
		 *     beforeValidateAttribute: undefined, // function (form, attribute) | boolean
		 *     afterValidateAttribute: undefined,  // function (form, attribute, data, hasError)
		 * }
		 */
		attributes: [],
		errorCallback: undefined
	};
})(jQuery);
