/*
   jQuery Ketchup Plugin
   =====================
   Tasty Form Validation
   
   Version 0.1 - 12. Feb 2010
   
   Copyright (c) 2010 by Sebastian Senf:
   http://mustardamus.com/
   http://usejquery.com/
   
   Dual licensed under the MIT and GPL licenses:
   http://www.opensource.org/licenses/mit-license.php
   http://www.gnu.org/licenses/gpl.html
   
   Demo:            http://demos.usejquery.com/ketchup-plugin/
   Source:          http://github.com/mustardamus/ketchup-plugin
   
   Edited by Yarick:
   * added option validateHidden - check validation for invisible items or not;
   * added option onFailed - callback function;
   * changed buildErrorList - swapped arguments, argument validations is not required now;
   * added $.fn.validateField - validate one field;
   * added connection to $.fn.jHintInput - get value of hint input on validation;
   * tab spaces changed to tabs;
*/

(function($) {
	var validate = 'validate';
	var firstField = null;
	
	function validateForm(form){
		var fields = fieldsToValidate(form);
		for(var i = 0; i < fields.length; i++) {
			bindField(fields[i]);
		}
		
		var tasty = true, halt = false;
		firstField = null;
		for(var i = 0; i < fields.length; i++) {
			if (!options.validateHidden && fields[i].is(':hidden')) continue;
			if (!options.validateDisabled && fields[i].is(':disabled')) continue;
			fields[i].trigger('ketchupBind', [ function(errList){
				if (errList.length){
					if (!firstField) firstField = fields[i];
					tasty = false;
					if (options.stopOnFirst) halt = true;
				}
			} ]);
			if (halt) break;
		}
		if(!tasty){
			if (options.onFailed && typeof options.onFailed=='function') 
				options.onFailed.call(form.get(0), firstField);
			return false;
		}
		return true;
	}
  
	function squeeze(form) {
		var isForm = form.get(0).tagName == 'FORM';
		if (isForm){
			if (form.data('ketchup-plugin')) return false;
			form.data('ketchup-plugin', true);
		}
		
		if (isForm){
			form.submit(function() {
				return validateForm(form);
			});
		} else {
			var res = validateForm(form);
			if (typeof options.onValidate == 'function') 
				options.onValidate.call(form.get(0), res);
		}
	}
  
	function fieldsToValidate(form) {
		var fields = [];
		$(form).find(':input['+options.validationAttribute+'*='+validate+']').each(function() {
			fields.push($(this));
		});
		return fields;
	}
  
	function bindField(field) {
		var errorContainer = field.after(options.errorContainer.clone()).next();
		var contOl = errorContainer.find('ol');
		var visibleContainer = false;

		/*
		$(window).resize(function() {
		  options.initialPositionContainer(errorContainer, field);
		}).trigger('resize');
		*/
		
		field.unbind('ketchupBind').bind('ketchupBind', function(e, fn){
			var errs = buildErrorList(field, extractValidations(field)), errList = errs.html;
			if (errList.length) {
				contOl.html(errList);
				if(!visibleContainer) {
					options.initialPositionContainer(errorContainer, field);
					//options.hideContainer(errorContainer, field);
					var res = options.showContainer(errorContainer, field, errs);
					visibleContainer = !res;
				}
				options.positionContainer(errorContainer, field);
			} else {
				options.hideContainer(errorContainer, field);
				visibleContainer = false;
			}
			if (typeof fn == 'function') fn.call(this, errList);
		});
		
		field.blur(function() {
			field.trigger('ketchupBind');
		});
    
		if (field.attr('type') == 'checkbox') {
			field.change(function() { //chrome dont fire blur on checkboxes, but change
				$(this).blur(); //so just simulate a blur
      		});
		}
	}
  
	function extractValidations(field) {
		var valStr = field.attr(options.validationAttribute);
		valStr = valStr.substr(valStr.indexOf(validate) + validate.length + 1);
		var validations = [];
		var tempStr = '';
		var openBrackets = 0;
		for (var i = 0; i < valStr.length; i++) {
			switch(valStr.substr(i, 1)) {	// IE7 valStr[i] does not work
				case ',':
					if(openBrackets) {
						tempStr += ',';
					} else {
						validations.push(trim(tempStr));
						tempStr = '';
					}
					break;
				case '(':
					tempStr += '(';
					openBrackets++;
					break;
				case ')':
					if(openBrackets) {
						tempStr += ')';
						openBrackets--;
					} else {
						validations.push(trim(tempStr));
					}
					break;
				default:
					tempStr += valStr.substr(i, 1);
			}
		}
		return validations;    
	}

	function trim(str) {
		return str.replace(/^\s+/, '').replace(/\s+$/, '');
	}

	function getFunctionName(validation) {
		if(validation.indexOf('(') != -1) {
			return validation.substr(0, validation.indexOf('('));
		} else {
			return validation;
		}
	}

	function buildParams(validation) {
		if(validation.indexOf('(') != -1) {
			var arr = validation.substring(validation.indexOf('(') + 1, validation.length - 1).split(',');
			var tempStr = '';
	  
			for(var i = 0; i < arr.length; i++) {
				var single = trim(arr[i]);
				if(parseInt(single)) {
					tempStr += ','+single;
				} else {
					tempStr += ',"'+single+'"'
				}
			}
	
			return tempStr;
		} else {
			return '';
		}
	}
  
	function formatMessage(message, params) {
		var args = message.split('$arg').length - 1;
		if (args) {
			var parArr = params.split(',');
			for(var i = 1; i < parArr.length; i++) {
				message = message.replace('$arg'+i, parArr[i]);
			}
		}
		return message;
	}
  
	function buildErrorList(field, validations) {
		if (!validations) validations = extractValidations(field);
		var list = '', funcs = new Array(), first = null;
		for(var i = 0; i < validations.length; i++) {
			var funcName = getFunctionName(validations[i]);
			var params = buildParams(validations[i]);
			var value = field.val();
			if ($.fn.jHintInput && field.data('jHintInput'))
				value = field.data('jHintInput').val();
			if (!eval('$.fn.ketchup.validations["'+funcName+'"](field, value'+params+')')){
				list += '<li>'+formatMessage($.fn.ketchup.messages[funcName]
					? $.fn.ketchup.messages[funcName] : 'ketchup.'+funcName, params)+'</li>';
				funcs.push(funcName);
				if (first == null) first = funcName;
			}
		}
		return {"html" : list, "funcs" : funcs, "first": first, "obj": 1};
	}
  
	var errorContainer = $('<div>', {
		'class':  'ketchup-error-container',
		html:     '<ol></ol><span></span>'
	});
  
	var initialPositionContainer = function(errorContainer, field) {
		var fOffset = options.position == 'absolute' ? field.position() : field.offset();
		errorContainer.css({
			left: fOffset.left + field.width() - 10,
			top: fOffset.top - errorContainer.height()
		});
	};
  
	var positionContainer = function(errorContainer, field) {
		var fOffset = options.position == 'absolute' ? field.position() : field.offset();
		errorContainer.animate({
			top: fOffset.top - errorContainer.height()
		});
	};
  
	var showContainer = function(errorContainer, field, errs) {
		var name = $(field).attr('name').replace(/[\[\]]/g, '-');
		var $empty = $('span#'+name+'-empty');
		var $error = $('span#'+name+'-error');
		if (!errs) errs = {first : null};
		if ($empty.size() && errs.first == 'required'){
			$error.hide();
			$empty.show();
			return true;
		} else if ($error.size()){
			$empty.hide();
			$error.show();
			return true;
		} else {
			errorContainer.fadeIn();
			errorContainer.hover(function(){
				$(this).hide();
			},function(){
				$(this).fadeIn();
			});
		}
		return false;
	};
  
	var hideContainer = function(errorContainer, field) {
		var name = $(field).attr('name').replace(/[\[\]]/g, '-');
		var $empty = $('span#'+name+'-empty');
		var $error = $('span#'+name+'-error');
		if ($empty.size()) $empty.hide();
		if ($error.size()) $error.hide();
		if (!$empty.size() && !$error.size()) errorContainer.fadeOut();
	};
  
	$.fn.ketchup = function(opt) {
		options = $.extend({}, $.fn.ketchup.defaults, opt);
		
		return this.each(function() {
			squeeze($(this));
		});
	};
  
	$.fn.ketchup.validation = function(name, func) {
		$.fn.ketchup.validations.push(name);
		$.fn.ketchup.validations[name] = func;
	};
 
	$.fn.ketchup.validateField = function(field){
		if (!options.validateHidden && $(field).is(':hidden')) return true;
		if (!options.validateDisabled && $(field).is(':disabled')) return true;
		var errs = buildErrorList($(field));
		if (errs.obj) return errs.html.length == 0;
		return errs.length == 0;
	}
	
	$.fn.ketchup.isValid = function(form){
		var fields = fieldsToValidate(form);
		var tasty = true;
		for (var i=0; i<fields.length; i++){
			if (!$.fn.ketchup.validateField(fields[i])){
				tasty = false;
				break;
			}
		}
		return tasty;
	}
  
	$.fn.ketchup.messages = {};
	$.fn.ketchup.validations = [];
	var options;

	$.fn.ketchup.defaults = {
		validationAttribute:		'class',
		errorContainer:				errorContainer,
		initialPositionContainer:	initialPositionContainer,
		positionContainer:			positionContainer,
		showContainer:				showContainer,
		hideContainer:				hideContainer,
		position:					'absolute',
		validateHidden:				true,
		validateDisabled:			false,
		onFailed:					null,
		onValidate:					null,
		stopOnFirst:				false
	};
})(jQuery);

