<!--//
String.prototype.ucFirst = function () {
   return this.substr(0,1).toUpperCase() + this.substr(1,this.length);
}

String.prototype.v_replace = function(pattern, replacement) {
  return this.split(pattern).join(replacement);
}

/**
 * The Msg object represents a front end ui message on a form field
 * This is also reflected at the backend by the MsgMgr php object 
 *   - it provides the data for for field highlighting, warning icons and text msg's
 */
var Msg = Class.create({
	initialize: function(params) {

		// if we have no field
		if (!params.id) {
			throw new Error("An id is required for the message", "An id is required for the message");
		}

		// set defaults
		this.id				= params.id;										// the id for this message
		this.field 			= params.field || params.id							// the field name (defaults to the id)
		this.title 			= params.title || (params.id).ucFirst();			// the title (defaults to the id ucfirst'd)
		this.text 			= params.text  || '';								// the text for the message
		this.level 			= params.level || 'ERROR';							// the level of message (defaults to ERROR)
		this.icon 			= params.icon || true;								// do we show an icon if required (defaults to true)
		this.panel			= params.panel || true;								// do we add the message onto the panel (defaults to true)
		this.highlight 		= params.highlight || false;						// do we add field highlighting (defaults to false)
		this.highlightClass = params.highlightClass || 'highlightField';		// the class used for highlight (defaults to highlightField)
	}, 

	toString: function() {
		return this.id + ':' + this.title + ':' + this.text + ':' + this.level + ':' + this.field + ':' + this.icon + ':' + this.hightlight;
	}
});

/**
 * The Rule object extends the Msg class to include a definition of the types of validation
 */
var Rule = Class.create(Msg, {
	initialize: function($super, params) {
		
		// if we have no field
		if (!params.type) {
			throw new Error("A type is required for a rule", "A type is required for a rule");
		}

		this.type = params.type
    	$super(params);
	}
});

var MsgMgr = Class.create({

	initialize: function() {
		
		// class for field highlighting
		this.highlightClass = 'highlightField';

		// array to hold the messages
		this.msgs = new Array();

		// array to hold the icons
		this.icons = Array();

		// the various levels of messages
		this.msgLevels = new Array('OK', 'INFO', 'WARN', 'ERROR', 'DEBUG');

	    for (var i=0; i < this.msgLevels.length; i++) {
    	    this.msgs[this.msgLevels[i]] = new Array();
	    }

	}, 

	/**
	 * @param field
	 * @param key
	 * @param msg
	 * @param level
	 * @param field
	 * @param highlight_field
	 */
	add: function(msg) {

        // remove any existing messages/icons/etc with this id
        this.clearMsgById(msg.id);
	
		// add the msg onto the stack
		this.msgs[msg.level].push(msg);

		// if the msg is to be written onto the panel
		if (msg.panel) {

			// make sure the panel exists
			if (!$('MsgPanels')) {
				if (!$('MsgPanelsPlaceHolder')) {
					throw new Error("MsgPanelsPlaceHolder is not defined", "MsgPanelsPlaceHolder is not defined");
				} else {
					$('MsgPanelsPlaceHolder').replace('<div id="MsgPanels"></div>');
				}
			}

			// the panel's name
			var panel       = 'MsgPanel' + msg.level.toLowerCase().ucFirst();

			// the uls id
			var ul_id   = msg.level + '_panel_ul';

			// if the panel for this level does not already exist
			if (!$(panel)) {

				//
				var e_panel     = new Element('div', {'id': panel, 'style':'zoom:1;display:block;'});

				//
				var e_ul        = new Element('ul', {'id': ul_id});

                // add the list onto the panel
				e_panel.appendChild(e_ul);

                // add the panel onto the list of panels
                $('MsgPanels').appendChild(e_panel);
			}

			// set the li id
			var li_id   = msg.level + '_panel_li_' + msg.id;
			var e_li    = new Element('li', {'id': li_id}).update('<strong>' + msg.title.v_replace(/_/g, ' ') + '</strong>&nbsp;-&nbsp;' + msg.text);

			// see does this work
			$(ul_id).appendChild(e_li);
		}

		// if the message has a field
		if (msg.field != '') {

			// do we want the icon
			if (msg.icon) {
                this.addIcon(msg.id, msg.field, msg.text, msg.level);
            }

			// do we want to highlight the field
			if (msg.highlight) {
				this.highlightField(msg.field, msg.highlightClass);
            }
		}
	}, 


	/**
	 * Sets a message on a field
	 * @param id
	 * @param field
	 * @param message
	 * @param level
	 */
	addIcon: function(id, field, msg, level) {	

		// make sure we have a field
		if (!$(field)) {
            return false;
        }

		// define the id
		var icon_id     = "field_icon_" + id;

		// if we already have and icon for this field
		if (this.icons[icon_id]) {

			// remove it
			removeIconById(id);
		}

		// we might want to add a popup div layer onto this and then remove the title from the icon
		var icon_html = '<span id="' + icon_id + '" class="msgMgrIcon' + level + '" title="' + msg + '"></span>';

		// add the field after the <span class="required"> or just after the field 
		if ($(field).next('span.required')) {
            new Insertion.After(($(field).next('span.required')), icon_html);
        } else {
            new Insertion.After(field, icon_html);
        }

		// we want to ad this onto a stack of icons
		this.icons.push(icon_id);
	},

	/**
	 * Removes an icon by its id
	 * @param msg id
	 */
	removeIconById: function(id) {

        // define the element id
        var icon_id = "field_icon_" + id;
        
		// if it exists (and it might not) - remove it
		if ($(icon_id)) {
			return $(icon_id).remove();
		}
	}, 

	/**
	 * Remove all the icons
	 */
	removeAllIcons: function() {
        $$('span[id^=field_icon_]').each(function(e) {e.remove();});
	},

	/**
	 * Remove message text
	 * @param msg id
	 */
	removeMsgTextById: function(id) {
	
		var li_id = "panel_li_" + id;

		if ($(li_id)) {
			return $(li_id).remove();
		}
	},

	/**
	 * Removes all content in the msg panels
	 */
	removeAllMsgText: function(id) {
	
		if ($('MsgPanels')) {
			$('MsgPanels').update('');
		}
	}, 

	
	/**
 	 * Clears the page messages for a specific option level
 	 * @param level (optional)
	 */
	clearAll: function(level) {

		// empty the messages
		this.removeAllMsgText();

		// remove all the field icons
		this.removeAllIcons();

		// clear any field highlighting
		$$('.' + this.highlightClass).each(function (element) {
			// this.removeHighlight(element)
		});

		// wipe the array
		this.msgs = null;
		this.msgs = Array();
		this.icons = Array();

		// populate the placeholders
		for (var i=0; i < this.msgLevels.length; i++) {
			this.msgs[this.msgLevels[i]] = new Array();
		}
	},


	/**
	 * Clear the error value
	 * @param id
	 */
	clearMsgById: function(id) {
	
		// removes a message by its id
		this.removeMsgTextById(id);

		// remove the icon
		this.removeIconById(id);

		// remove the message from the array
		this.removeMsgById(id);

		// return
		return true;
	},

	/**
	 * @param id
	 */
	removeMsgById: function(id) {

		// remove from the array
		for(var l=0; l < this.msgLevels.length; l++) {
		
			// assign the level
			var level = this.msgLevels[l];
		
			// for each of the messages
			for (var i=0; i < this.msgs[level].length; i++) {

				// 
				if (this.msgs[level][i]['id'] == id) {

					// set the matching value to null
					this.msgs[level][i] = null;
	
					// remove the null 
					this.msgs[level] = this.msgs[level].compact();
					
					// 
					return true;
				}
			}
		}
	}, 

	/**
	 * Add highlighting to a field
	 */
	highlightField: function(field, highlightClass) {
		if ($(field)) {
			$(field).addClassName(highlightClass);
		}
	},
	
	/**
	 * Remove highlighting from a field
	 */
	removeHighlight: function(field) {
		if ($(field)) {
			$(field).removeClassName(highlightClass);
		}
	}, 

	/**
	 * Returns true if messages exist
	 */
	hasMsgs: function(level) {

		// if we page a level in
		if (level) {

			// return true or false if we have message
			return (this.msgs[level].length > 0) ? true : false;

		// otherwise
		} else {

			this.msgLevels.each(function (level) {
				if (this.msgs[level].length > 0) {
					return true;
				}
			});

		}
		return false;
	}
});

/**
 * The Validator Object accepts a collections of rules to validate against fields on a form
 * Custom Validation Rules can be added
 * By default it searches the form for elements with a class="required" and uses default msgs for them 
 * unless overridden by a custom rule
 */
var Validator = Class.create(MsgMgr, {

	initialize: function($super) {

		// the validation rules
		this.rules				= new Array();

		// to hold any field errors to avoid duplicate messages
		this.errors				= new Array();

		// the regular expressions
		this.regexps 			= new Array();
		this.regexps['email'] 	= /^([^@\s]+)@((?:[-a-zA-Z0-9]+\.)+[a-zA-Z]{2,})$/;
		this.regexps['mac']		= /^(([a-fA-F0-9]{2})(:|-|)?){5}[a-fA-F0-9]{2}$/;
		this.regexps['int'] 	=  /^\d+$/;
		this.regexps['time'] 	=  /^\d{1,2}:\d\d+$/;

		// call MsgMgr's constructor
		$super();
	}, 

	clear: function() {

		// clear all the messages
		this.clearAll();

		// clear all rules
		this.rules.clear();

		// clear the errors
		this.errors.clear();
	}, 

	/**
	 * Add a rule onto the stack for validation
	 * @param Rule
	 */
	addRule: function(rule) {
		this.rules.push(rule);
	},

	alreadyErrored: function (id) {
		for (var i=0; i< this.errors.length; i++) {
			if (this.errors[i] == id) { 
				return true; 
			}
		} 
		return false;
	},

	customRuleExists: function(id, type) {

		for(i=0;i<this.rules.length;i++) {
			if (this.rules[i].id == id && this.rules[i].type == type) {
				return true;	
			}
		}

		return false;
	},

	validateForm: function(formID) {

		// get all the fields for this form
		var fields = Form.getElements(formID);

		// for each of them
		for(var i=0;i<fields.length;i++) {

			// get the classes for this field
			var classes = $w(fields[i].className);

			// for each of the classes
			for (var j=0; j< classes.length; j++) {

				// add rules for the ones we care about
				switch (classes[j]) {

					case 'required':

						// if a custom rule does not already exist for this type (required in this case)
						if (!this.customRuleExists(fields[i].id, 'required')) {

							// add a default rule for this field
							this.addRule(new Rule({id: fields[i].id, type:'required', text:'Please enter - ' + fields[i].id}));
						}

						break;

					case 'email':
					
						// if a custom rule does not already exist for this type (required in this case)
						if (!this.customRuleExists(fields[i].id, 'email')) {
		
							// add a default rule for this field
							this.addRule(new Rule({id: fields[i].id, type:'email', text:'The email address is invalid'}));
						}
						break;

	
					case 'time':
				
						// if a custom rule does not already exist for this type (required in this case)
						if (!this.customRuleExists(fields[i].id, 'time')) {
		
							// add a default rule for this field
							this.addRule(new Rule({id: fields[i].id, type:'time', text:'The time should be in the format (HH:MM)'}));
						}
						break;

	
					/*** a default class could check the regex's array for a key and if exists do that  better than adding new cases statements */
				}
			}
		}

		// run each of the validation rules
		for(i=0;i<this.rules.length;i++) {

			try {

				// we don't want to validate a field that has already had a rule applied and failed on it (i don't think)
				if (this.alreadyErrored(this.rules[i].id)) {
					continue;
				}
				
				// if the field does not validate 
				if (!this.validateField(this.rules[i])) {

					// then add it to the errors list so that we don't 
					// add a second sequential error onto it
					// this means the classNames should be in order
					// eg 'required email' not 'email required'
					this.errors.push(this.rules[i].id);
				}
			} catch (msg) {
				alert('Validator Error: ' + msg.message + ' on ' + this.rules[i].id + ' of type ' + this.rules[i].type);
			}
		}

		// does the form validate = does it have 0 msgs of level ERROR
		return (!this.hasMsgs('ERROR'));
	},


	/**
	 * Only performs validation on the custom rules
	 * handy if you want a multi phase validation
	 */
	validateCustomRules: function() {

		// run each of the validation rules
		for(i=0;i<this.rules.length;i++) {

			try {

				// we don't want to validate a field that has already had a rule applied and failed on it (i don't think)
				if (this.alreadyErrored(this.rules[i].id)) {
					continue;
				}
				
				// if the field does not validate 
				if (!this.validateField(this.rules[i])) {

					// then add it to the errors list so that we don't 
					// add a second sequential error onto it
					// this means the classNames should be in order
					// eg 'required email' not 'email required'
					this.errors.push(this.rules[i].id);
				}
			} catch (msg) {
				alert('Validator Error: ' + msg.message + ' on ' + this.rules[i].id + ' of type ' + this.rules[i].type);
			}
		}

		// does the form validate = does it have 0 msgs of level ERROR
		return (!this.hasMsgs('ERROR'));


	}, 

	/**
	 *  @param Rule the rule to validate the field agains	 
	 */
	validateField: function(rule) {

		//
		switch(rule.type) {

			case 'required':

				if (!$F(rule.id)) {

                    title   = rule.title || '';
                    text    = rule.text || ' - ' +  field + ' is required';
                    level   = rule.level || '';

                    // add the error message
					this.add(new Msg({id:rule.id, title:title, text:text, level:level}));

					return false;
				}
				break;

			case 'email':

				if (!($F(rule.id).match(this.regexps['email']))) {

					title	= rule.title || '';
					text 	= rule.text || ' - The email address is invalid';
					level	= rule.level || '';
					
					// add the error message
					this.add(new Msg({id:rule.id, title:title, text:text, level:level}));

					// return false
					return false;
				}
				break;

			case 'selected':
				if ($(rule.id).options.selectedIndex == 0) {
					title	= rule.title || '';
					text 	= rule.text || ' - ' +  field + ' has not been selected';
					level	= rule.level || '';
					
					// add the error message
					this.add(new Msg({id:rule.id, title:title, text:text, level:level}));

					return false;
				}
				break;

			case 'int':

				if (!($F(rule.id).match(this.regexps['int']))) {

					title	= rule.title || '';
					text 	= rule.text || ' - The number is invalid';
					level	= rule.level || '';
					
					// add the error message
					this.add(new Msg({id:rule.id, title:title, text:text, level:level}));

					// return false
					return false;
				}
				break;

			case 'time':

				if (!($F(rule.id).match(this.regexps['time']))) {

					title	= rule.title || '';
					text 	= rule.text || ' - The time is invalid';
					level	= rule.level || '';
					
					// add the error message
					this.add(new Msg({id:rule.id, title:title, text:text, level:level}));

					// return false
					return false;
				}
				break;


			case 'strong_password':

				var password = $F(rule.id);

				if (!(password.length >= 8)) {
					this.add(new Msg({id:rule.id + '_short', title:'Password', text:'The password should be 8 or more alpha numeric chars'}));;
					return false;
				}

				if (!(password.match(/\d/))) {
					this.add(new Msg({id:rule.id + '_short', title:'Password', text:'The password should be 8 or more alpha numeric chars'}));;
					return false;
				}

				if (!(password.match(/\d/))) {
					this.add(new Msg({id:rule.id + '_alphanum', title:'Password', text:'The password requires at least 1 number'}));;
					return false;
				}

				if (!(password.match(/[A-Z]/))) {
					this.add(new Msg({id:rule.id + '_upper', title:'Password', text:'The password requires at least 1 upper case letter'}));;
					return false;
				}

				if (!(password.match(/[a-z]/))) {
					this.add(new Msg({id:rule.id + '_lower', title:'Password', text:'The password requires at least 1 lower case letter'}));;
					return false;
				}

				break;

			default:
				throw new Error("Unknown Validator type" + type, "Unknown Validator Type" + type);
				break;
		}
		
		// if we have got to here - ensure we remove any errors relating to this field
		this.clearMsgById(rule.id);

		// return true allowing validation success
		return true;
	}, 


	/**
	 * Simple function to match 2 fields (passwords)
	 */ 
	fieldsMatch: function(f_a, f_b, f_title, f_text) {
		try { 
			if (!($F(f_a) == $F(f_b))) {
				this.add(new Msg({id:f_a, title:f_title, text:f_text}));
			}
		} catch (msg) {
			alert(msg.error);
			throw new Error('Validator.fieldsMatch Error', 'Validator.fieldsMatch Error');
		}
	}
});

// create the validator
v = new Validator();

// on load - if we do not have a MsgPanels defined - replace with the placeholder
Event.observe(window, 'load', function() {
	if (!$('MsgPanels')) {
		if (!$('MsgPanelsPlaceHolder')) {
			// instead insert the msg panels straight after the header?
			throw new Error("MsgPanelsPlaceHolder is not defined", "MsgPanelsPlaceHolder is not defined");
		} else {
			$('MsgPanelsPlaceHolder').replace('<div id="MsgPanels"></div>');
		}
	}
});
