/**
 * @author		: Trevor Lemon
 * @modified	: 2012-02-14
 * @website		: http://conceptual-eyes.com
 * 
 * Basically this is a script that gets rid of all the tedious field and 
 * error checking of form input fields. There are a few ways that you implement
 * this sript.
 *
 * ## Required ##
 * @param	$form		The form containing the input elements.
 * @param	$msg		The message you want displayed while it's processing.
 * @param	$msgParent	(not required) If defined, the message will be added to this element (within the form) instead of the form body.
 * ## Optional ##
 * @param	$post		True or False. If true, info will be posted to script instead of using AJAX
 * @param	$errors		True or False. If true, will not POST or GET, will just return error count.
 * @param	$errorMsg	If not null, will override the standard 'there are this many errors' message.
 *
 * ## Important: jQuery must be included before forms.js ##
 * Method 1: Add this function to the onsubmit of your form <form action="" onsubmit="validateForm(this, 'Processing Form...', '#messageElement');">
 * Method 2: Override the function through jQuery jq(document).ready(function(){ jq('#formTest').bind('submit', function(e){ return validateForm(e.currentTarget, 'Processing...', '#msgHere'); }); });
 */

jq(document).ready(function(){
/* ------------------------------------ Form stuff -- */
	/**
	 * Since IE is a POS, there needs to be a 'real' and 'fake' password field if 
	 * you want to have text in a password field before the user selects the field
	 * and starts typing.
	 */
	jq('input[type=password]').each(function(){
		var p = jq(this);
		if(!p.hasClass('ignore')){
			var style = p.copyStyle('color');
			var c = (p.hasClass('default')) ? ' class="default"' : '';
			var f = jq('<input style="'+style+'" type="text" name="fakePass"'+c+' />');
			p.before(f);
			
			if(c != ''){
				// give functionality to the fake pass field
				f.focus(function(){
					// assign functionality to the 'real' password field
					p.blur(function(){ if(p.val() == ''){ p.hide(); f.show().blur(); } });
					p.show().focus();
					f.hide();
				});
			}
			
			if(p.val() == '' && c != ''){ p.hide(); }
			else{ f.focus().hide(); }
		}
	});
	
	// keep track of which submit button is clicked
	jq('[type="submit"]').live('click', function(e){
 		jq(jq(this).parents('form')[0]).data( 'subClick', jq(this).val() );
	});
	
	// Assign default text to all fields that have a default set.
	jq('.default').each(function(){ jq(this).assignDefault(); });
});

// Add chainable functions to JQ
jq.fn.extend({
// -- Assign default text to input areas -- \\
	assignDefault: function(){
		var el = jq(this);
		var isInput = el.is(':input');
		
		// hide default text, and keep hidden if a value is present.
		el.focus(function(){
			el = jq(this);
			if(el.hasClass('default')){
				el.removeClass('default');
				(isInput) ? el.val('') : el.html('');
			}
		});
		// show default text if no value is present
		el.blur(function(){ 
			el = jq(this);
			if(isInput){ if(el.val() == ''){ 
				var val = (el.is('textarea'))
					? el.attr('title').replace('<br/>', "\n")
					: el.attr('title');
				el.addClass('default').val(val); } 
			}else{ if(el.html() == ''){ el.addClass('default').html(el.attr('title')); } }
		});
		// assign default value from title text
		if(isInput){
			if(el.val() == '' || el.val() == el.attr('title')){ 
				var val = (el.is('textarea'))
					? el.attr('title').replace('<br/>', "\n")
					: el.attr('title');
				el.val(val);
			}else{ el.removeClass('default'); }
		}else{
			if(el.html() == ''){ el.html(el.attr('title')); }
			else{ el.removeClass('default'); }
		}
		
		return this;
	},
// -- Message box functionality -- \\
    /**
     * Create a new message and adds it to the caller.
     * @param	$msg		The message you want to display.
     * @param	$class		Three states. 'success', 'processing', 'error'
     * @param	$msgParent	If defined, the message will be added to this element. String.
     * @return	The JQ object to make the function chainable
     */
	newMessage: function($msg, $class, $msgParent){
		var msg  = '<div class="msg '+$class+'">';
			msg +=		'<div class="icon"></div>';
			msg +=		'<div id="msgText">'+$msg+'</div>';
			msg += '</div>';
		var form = jq(this);
			form.data('msg', { msgEl:jq(msg) } ); // store the message to the form for future access
		
		// add the message
		($msgParent != undefined)
			? form.data('msg').msgEl.appendTo(form.find($msgParent))
			: form.data('msg').msgEl.appendTo(form);
		
		return this;
	},
	/**
	 * Updates the message content without having to re-create elements
	 * @param	$msg	The message you want to display.
     * @param	$class	Three states. 'success', 'processing', 'error'
     * @return	The JQ object to make the function chainable
     */
	changeMessage: function($msg, $class){
		var msg = jq(this).data('msg').msgEl;
		msg.find('#msgText').html($msg);
		msg.attr('class', 'msg '+$class);
		return this;
	},
	/**
	 * Removes the message
	 * @return	The JQ object to make the function chainable
	 */
	removeMessage: function(){
 		if(jq(this).data('msg') != undefined){ 
 			jq(this).data('msg').msgEl.remove();
 			jq(this).data('msg', null);
 		}
   		return this;
    },
// -- Element functionality -- \\
	/**
	 * Copy an elements style
	 * @param	$excludedAtts	Optional, can be a string (one item), or an array of multiple css properties to not copy.
	 * @return	String	all the css properties assigned to object.
	 */
	copyStyle: function($excludedAtts){
		var atts = ['width','height','font','color','font-size','font-weight','margin-left','margin-right','margin-top','margin-bottom','padding','padding-left','padding-right','padding-top','padding-bottom','border-left','border-right','border-top','border-bottom','border-color','border-weight','border-top-right-radius','border-top-left-radius','border-bottom-right-radius','border-bottom-left-radius'];
		// remove the attributes that need to be excluded
		if($excludedAtts != undefined){
			var ex = ( !jq.isArray($excludedAtts) ) ? [$excludedAtts] : $excludedAtts;
			jq(ex).each(function(key, val){ for(var i=atts.length-1; i>=0; i--){ if(atts[i] == val){ atts.splice(i, 1); } } });
		}
		var el = jq(this);
		var style = '';
		// copy all the styles that have values
		jq(atts).each(function(key, val){ if(el.css(val) != '' && el.css(val) != undefined){ 
			var cssVal = el.css(val);
			// account for IE being a POS
			if(val == 'width' || val == 'height' && jq.browser.msie && jq.browser.version <= 7){
				switch(val){
					case 'width'	: cssVal = el.width()+4; break;
					case 'height' 	: cssVal = el.height()+4; break;
				}
			}
			
			style += val+':'+cssVal+' !important; '; } });
		return style;
	},
	
	/**
	 * Adds an error icon next to an input element that's in an error state.
	 * @return	The JQ object to make the function chainable
	 */
	addErrorIcon: function(){
		var el = jq(this);
		var wasHidden = false;
		if(el.is(':hidden')){ el.show(); wasHidden = true; }
		var p = el.position();
		if(wasHidden){ el.hide(); }
		var icon = jq('<div class="errIcon"></div>').appendTo(el.parent());
		icon.css('top', p.top+2).css('left', (p.left + el.outerWidth()) - (icon.outerWidth()/2) );
		el.focus(function(){ icon.remove(); });
		return this;
	},
	/**
	 * Remove all error icons from input elements that were in an error state.
	 * @return	The JQ object to make the function chainable
	 */
	removeErrorIcons: function(){
		jq(this).find('.errIcon').each(function(){ jq(this).remove(); });
		return this;
	},
	/**
	 * Adds the error state to all invalid input fields and adds the error message
	 * @param	$form		The form that contains the errors
	 * @param	$errorMsg	If not null, will override the standard error message.
	 * @return	The JQ object to make the function chainable
	 */
	addErrorState: function($form, $errorMsg){
		var errors = ( $form.data('errors') == undefined ) ? 0 : $form.data('errors');
		errors++;
		$form.data('errors', errors);
		var el = jq(this);
		// assign the function if not assigned
		if( !isSet ){ el.bind('focus', function(e){ jq.fn.removeErrorState(e, $form, $errorMsg); }); }
		var errMsg = ($errorMsg != null) ? $errorMsg : jq.fn.errorCountMsg(errors);
		if($form.data('msg')){ $form.changeMessage(errMsg, 'error'); }
		else{ $form.newMessage(errMsg, 'error', $form.data('msgParent')); }
		el.addClass('error').addErrorIcon();
		// check if function already assigned
		var isSet = false;
		if(el.data('events') != undefined){ jq(el.data('events').focus).each(function(key, obj){ if(obj.handler == jq.fn.removeErrorState){ isSet = true; } }); }
		return this;
	},
	
	/**
	 * Removes the error state of an input field when the user selects it
	 * @param	$e			The focus event
	 * @param	$errorMsg	If not null, will override the standard error message.
	 * @param	$form		The form that contains the error message.
	 */
	removeErrorState: function($e, $form, $errorMsg){	
		var e = jq($e.currentTarget);
		if(e.hasClass('error')){
			e.removeClass('error');
			if(e.attr('name') == 'fakePass'){ e.parent().find('input[type="password"]').removeClass('error'); }
			$form.data('errors', $form.data('errors') - 1);
			var errors = $form.data('errors');
			if(errors == 0){ $form.trigger('noErrors'); }
			else{ if($errorMsg == null){ $form.changeMessage(jq.fn.errorCountMsg(errors), 'error'); } }
		}
		return this;
	},
	/**
	 * Constructs a gramatically correct message about how many form errors there are.
	 * @param	$errors		The number of errors in the form.
	 */
	errorCountMsg: function($errors){
		var plural = ($errors > 1) ? 's are' : ' is';
		return $errors+' field'+plural+' invalid.';
	},
	/** Resets a form */
	reset: function(){
		jq(this).each(function(){ this.reset(); });
		return this;
	}
});

/**
 * Validates any input with the class of 'validate'. If all
 * the elements that need validation aren't valid, then an
 * error message is returned. If the elements have info, the
 * data is sent to the processing script, if success, an
 * array with some actions are returned, if error, just the
 * error message is returned. Further explainations of the 
 * actions returned on success are described below, above
 * the 'formResponse' function.
 * @param	$form		The form containing the input elements.
 * @param	$msg		The message you want displayed while it's processing.
 * @param	$msgParent	(not required) If defined, the message will be added to this element (within the form) instead of the form body.
 * @param	$post		True or False. If true, info will be posted to script instead of using AJAX
 * @param	$errors		True or False. If true, will not POST or GET, will just return error count.
 * @param	$errorMsg	If not null, will override the standard 'there are this many errors' message.
 * 
 * This function sends out these events ['noErrors', 'formCleared', 'errorsReturned']
 * You can listen and respond to them like so jq('form_id_or_class').live('errorsReturned', function(e){ console.log(e); });
 *
 */
function validateForm($form, $msg, $msgParent, $post, $errors, $errorMsg){
	var stopFunc = false;
	if($form == undefined){ alert('You need to pass the form into this function.'); stopFunc = true; }
	if($msg == undefined){ alert('You need define a message string.'); stopFunc = true; }
	
	// clear previous errors
	if( jq($form).data('errors') != undefined ) jq($form).data('errors', 0);
	
	if(!stopFunc){
		var f = jq($form);
		var invalid = [];
		var data = {};
		
		// clear focus, so that if there is an error on an input, and you select it, the error will be cleared
		f.find('*:focus').blur();
		
		// this enables me to listen for when the error message has been removed.
		f.bind('noErrors', function(e){ f.removeMessage(); });
		// listen for when the form was cleared
		f.bind('formCleared', function(e){});
		// listen in case errors are returned from a script
		f.bind('errorsReturned', function(e){});
		// fired when form has started proccessing
		f.bind('proccessing', function(e){});
		
		// form is proccessing
		f.trigger({ type:'proccessing' });
		
		f.find('*[name]').each(function(){ 
			var el = jq(this);
			
			// this skips any submit buttons that weren't clicked
			if(el.attr('type') == 'submit' && el.val() != f.data('subClick')){ return true; }
			
			if(el.hasClass('validate') && !validateFormVal(el)){ 
				// account for 'fake' password fields
				if(el.attr('type') == 'password' && !el.hasClass('ignore')){ invalid.push( el.parent().find('input[name=fakePass]') ); }
				if(!el.hasClass('ignore')){ invalid.push(el); }
			}else{ 
				var n = getObjAtt(el);
				parseData(n, data, el);
			}
		});
		
		//console.log('| '+location.protocol, '| '+location.host, '| '+location.pathname, '| '+location.href);
		data.caller = location.pathname.split('/')[1]; // send what page called the script
		
		f.removeMessage().removeErrorIcons(); // clear the old message
		
		if(invalid.length > 0){
			f.data('msgParent', $msgParent);
			// mark all invalid fields
			for(var i=0; i<invalid.length; i++){
				var el = invalid[i];
				// don't count password fields since we'll use the 'fake' ones.
				if(el.attr('type') != 'password' || el.hasClass('ignore')){ el.addErrorState(f, $errorMsg); }
			}
			var errors = f.data('errors');
			if($errors == true){ return errors; }
		}else{
			f.newMessage($msg, 'processing', $msgParent);
			if($errors == undefined || $errors == false){
				($post != undefined && $post == true)
					? f[0].submit()
					: jq.get(f.attr('action'), data, formResponse );
			}else{
				return invalid.length;
			}
		}
	}
	/**
	 * Determines if an element has an array for a name or just a string, then returns the name of the item, not the full array name.
	 * @param	$el		The element we're trying to get the name from
	 * @return	Object/String	An object containing the main object name and it's current attribute name OR the name of the input
	 */
	function getObjAtt($el){
		var match = $el.attr('name').match( /([a-z]+)\[(.*?)\]/i ); // get the name of the object and it's attribute
		return (match != null) ? { obj:match[1], att:match[2] } : $el.attr('name');
	}
	
	/**
	 * Will parse the data into a format for AJAX
	 * @param	$obj	The data object that the data is added to.
	 * @param	$arr	The data array that the data is added to.
	 * @param	$el		The input element that contains the value.
	 */ 
	function parseData($obj, $arr, $el){
		if($obj.obj){
			if($arr[$obj.obj] == undefined) $arr[$obj.obj] = {};
			if($el.val() != ''){ 
				// account for checkboxes & radials
				var skip = false;
				if($el.attr('type') == 'checkbox' || $el.attr('type') == 'radio'){ if(!$el.is(':checked')){ skip = true; } }
				if(!skip){
					if($el.attr('type') == 'checkbox'){
						($arr[$obj.obj][$obj.att] == undefined)
							? $arr[$obj.obj][$obj.att] = [$el.val()]
							: $arr[$obj.obj][$obj.att].push($el.val());
					}else{ $arr[$obj.obj][$obj.att] = $el.val(); }
				}
			}
		}else{ $arr[ $obj ] = $el.val(); }
	}
	
	/**
	 * Responses:
	 * success	: True, False
	 * msg		: The message returned from the processing script
	 * action	: What to do if success.
	 *				refresh	: Reloads the page. Good for processing scripts that set Session vars
 	 *				goto	: Loads a specified url.
	 * url		: The location to load if action equals 'goto'.
	 */
	function formResponse($response){
		var r = eval($response);
		if(r.success){
			// execute response action
			switch(r.action){
				case 'refresh' 	: location.reload(); break;
				case 'goto'		: location = r.url; break;
				default 		: 
					clearForm(r);
					// show response message
					f.changeMessage(r.msg, 'success');
					// wait 3 seconds then kill the message
					window.setTimeout( function(){ f.removeMessage(); f.trigger({ type:'noErrors' }); }, 3000 );
			}
		}else{
			var msg = ($errorMsg != null) ? $errorMsg : r.msg;
			f.changeMessage(msg, 'error');
			// ignore the standard form response properties, just add msg and any other custom props
			var obj = { type:'errorsReturned' };
			for(var i in r){ if(i != 'success' || i != 'action'){ obj[i] = r[i]; } }
			f.trigger(obj);
		}
	}
	
	function clearForm($r){
		f.reset();
		f.find('input[type="password"]').blur(); // account for fake password fields
		f.trigger({ type:'formCleared', response:$r });
	}
	
	return false; // this is so the form doesn't submit the data until we say so.
}
/**
 * Validates an inputs value
 * @param	$el	The input element
 * @return	Boolean		False if there are errors
 */
function validateFormVal($el){
	if($el.val() == '' || $el.hasClass('default')){ return false; }
	else if($el.hasClass('email')){ 
		var emailRegEx = /^\w(\.?[\w-])*@\w(\.?[\w-])*\.[a-z]{2,6}$/i;
		return ( $el.val().search(emailRegEx) == -1 ) ? false : true;
	}else if($el.hasClass('user')){
		// verify that user name isn't in use
		return ( jq('#userMsg').text() != '' ) ? false : true;
	}else{ return true; }
}
