/**
 * @author brendan
 */

/**
 * @return {Function}
 */
jsapp = function() {
	return {
		/**
		 * @alias jsapp.namespace
		 * @param {String} strNamespace
		 */
		namespace: function(strNamespace) {
			//console.debug('namespace ' + strNamespace);
			var tokens = strNamespace.split('.');
			var o = window;
			
			while(token = tokens.shift()) {
				if (!o[token]) {
					//console.debug('make ' + token);
					//alert('make ' + token);
					o[token] = {};
				}
				o = o[token];
			}
		},
		
		/**
		 * @alias jsapp.extend
		 * @param {Object} objOriginal
		 * @param {Object} objExtensions
		 */
		extend: function(objOriginal, objExtensions) {
			for (var i in objExtensions) {
				objOriginal[i] = objExtensions[i];
			}
			return objOriginal;
		}	
	}	
}();
/**
 * @author brendan
 */
jsapp.namespace('jsapp.event');

/**
 * @return {Function}
 */
jsapp.event.EventDispatcher = function() {
	var that = {
		
		/**
		 * @alias jsapp.event.EventDispatcher.prototype.addEventListener
		 * @param {String} strEventName
		 * @param {Object} objListener
		 * @param {Object} objHandler
		 */
		addEventListener:function(strEventName, objListener, objHandler) {
			var listeners = getListenerList(strEventName);
			
			this.removeEventListener(strEventName, objListener, objHandler);
			
			listeners.push({obj:objListener, handler:objHandler});	
		},
		
		/**
		 * @alias jsapp.event.EventDispatcher.prototype.removeEventListener
		 * @param {String} strEventName
		 * @param {Object} objListener
		 * @param {Object} objHandler
		 * @return {Boolean}
		 */
		removeEventListener:function(strEventName, objListener, objHandler) {
			var listeners = getListenerList(strEventName);
			
			for (var i = 0; i < listeners.length; i++) {
				if (listeners[i].obj == objListener && listeners[i].handler == objHandler) {
					listeners.splice(i,1);
					return true;
				}
			}
			
			return false;
		},
		
		/**
		 * @alias jsapp.event.EventDispatcher.prototype.dispatchEvent
		 * @param {Object} objEvent
		 */
		dispatchEvent:function(objEvent) {
			//console.debug("Dispatch " );
			//console.dir(objEvent);
			if (objEvent.type) {
				if (!objEvent.target) {
					objEvent.target = this;
				}
			}
			
			var listeners = getListenerList(objEvent.type);
			
			for (var i = listeners.length - 1; i >= 0; i--) {
				var obj = listeners[i].obj;
				var handler = listeners[i].handler;
				
				if (typeof obj == 'function') {
					obj.apply(obj, [objEvent]);
				} else if (handler == null) {
					obj[objEvent.type](objEvent);
				} else if (typeof handler == 'string') {
					obj[handler](objEvent);
				} else if (typeof handler == 'function') {
					handler.apply(obj, [objEvent]);
				}
			}
		}
		
	};
	
	function getListenerList(strEventName) {
		if (!that['__' + strEventName + 'Listeners']) {
			that['__' + strEventName + 'Listeners'] = [];
		}
		return that['__' + strEventName + 'Listeners'];
	} 
	
	return that;
}/**
 * @author brendan
 */
jsapp.namespace('jsapp.validation');

jsapp.validation.EmailValidator = function() 
{
	var EmailValidator = function(strElementId) {
		
		var knownTLDs = /^(com|net|org|edu|int|mil|gov|arpa|biz|aero|name|coop|info|pro|museum)$/;
		var ipPattern = /^\[(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\]$/;
		var exceptions = [];
		
		var that = jsapp.extend(jsapp.validation.Validator(strElementId), {		
			missingAtSignError: 'Email addresses must contain at least one @ symbol',
			tooManyAtSignsError: 'Email addresses may contain only one @ symbol',
			noUsernameError: 'Email addresses must contain a username',
			illegalCharError: 'Email address contains an illegal character',
			missingPeriodInDomainError: 'Email domain is missing a peiod',
			illegalPeriodsInDomainError: 'Email domain may not contain ..',
			invalidDomainError: 'Email domain is invalid',
			invalidIPError: 'Invalid IP addres',
			
			addException: function(strValue) {
				exceptions.push(strValue);
			},
			
			doValidation: function(strValue) {
				var results = [];
				
				// Validate required first
				if (this.required) {
					var r = this.validateRequired(strValue);
					if (r) {
						results.push(r);
						return results;
					}
				}
				
				// Check if the value is an exception
				for (var i = 0; i < exceptions.length; i++) {
					if (strValue == exceptions[i]) {
						return results;
					}
				}
				
				// Check for presence of '@' character...
				var atPos = strValue.indexOf('@');
				
				if (atPos == -1) {
					results.push(jsapp.validation.ValidationResult(true, '', this.missingAtSignError))
					return results;
				} else if (strValue.indexOf('@', atPos + 1) != -1) {
					// Ensure only one '@'
					results.push(jsapp.validation.ValidationResult(true, '', this.tooManyAtSignsError))
					return results;
				}
				
				var username = strValue.substring(0, atPos);
				var domain   = strValue.substring(atPos + 1);
				
				// Ensure at least one char and no illegal chars in
				// username
				if (username.length == 0) {
					results.push(jsapp.validation.ValidationResult(true, '', this.noUsernameError))
					return results;
				}
				
				for (var i = 0; i < username.length; i++) {
					if (jsapp.validation.EmailValidator.ILLEGAL_CHARACTERS.indexOf(username.charAt(i)) != -1) {
						results.push(jsapp.validation.ValidationResult(true, '', this.illegalCharError))
						return results;
					}
				} 
				
				// Check if domain is IP address				
				var arr = domain.match(ipPattern);				
						
				if (arr != null) {
					for (var i = 1; i <= 4; i++) {
						console.debug(arr[i]);
						if (arr[i] > 255) {
							results.push(jsapp.validation.ValidationResult(true, '', this.invalidIPError))
							return results;
						}
					}
				} else {
					// Ensure at least one period in domain...
					var pp = domain.indexOf('.');
					
					if (pp == -1) {
						results.push(jsapp.validation.ValidationResult(true, '', this.missingPeriodInDomainError))
						return results;
					}
					
					// Ensure first character in domain is not '.'
					if (pp == 0) {
						results.push(jsapp.validation.ValidationResult(true, '', this.invalidDomainError))
						return results;
					}
					
					// Ensure no '..' in domain
					var dp = domain.indexOf('..');
					
					if (dp != -1) {
						results.push(jsapp.validation.ValidationResult(true, '', this.illegalPeriodsInDomainError))
						return results;
					}
					
					// Ensure valid TLD
					var domainTokens = domain.split('.');
					var tld = domainTokens.pop();
					console.debug('tld = ' + tld);
					if (tld.length != 2 && 
						tld.search(knownTLDs) == -1) {
						results.push(jsapp.validation.ValidationResult(true, '', this.invalidDomainError))
						return results;
					}
					
					// Ensure no illegal chars in domain
					for (var i = 0; i < domain.length; i++) {
						if (jsapp.validation.EmailValidator.ILLEGAL_CHARACTERS.indexOf(domain.charAt(i)) != -1) {
							results.push(jsapp.validation.ValidationResult(true, '', this.illegalCharError))
							return results;
						}
					} 
				}
				
				return results;
			}
		});
		
		return that;
	}
	
	EmailValidator.ILLEGAL_CHARACTERS = "()<>,;:\\\"[] `~!#$%^&*+={}|/?'";
	
	return EmailValidator;
}();
/**
 * @author brendan
 */
jsapp.namespace('jsapp.validation');

jsapp.validation.StringValidator = function(strElementId, minLength, maxLength) 
{
	if (minLength == null) minLength = 0;	
	if (maxLength == null) maxLength = -1;

	return jsapp.extend(jsapp.validation.Validator(strElementId), {
		tooLongError: 'This field must contain no more than ' + maxLength + ' characters.',
		tooShortError: 'This field must contain no less than ' + minLength + ' characters.',
		minLength: minLength,
		maxLength:	maxLength,	

		doValidation: function(strValue) {
			var results = [];
		
			if (this.required) {
				var r = this.validateRequired(strValue);
				if (r) {
					results.push(r);
				}
			}
		
			if (strValue.length < this.minLength) {
				results.push(jsapp.validation.ValidationResult(true, '', this.tooShortError));
			}
	
			if (this.maxLength > -1 && strValue.length > this.maxLength) {
				results.push(jsapp.validation.ValidationResult(true, '', this.tooLongError));
			}
		
			return results;
		}
	});
}/**
 * @author brendan
 */
jsapp.namespace('jsapp.validation');

/**
 * @return {Function}
 */
jsapp.validation.Validator = function(strElementId) {

	var _trigger 		= null;
	var	_triggerEvent 	= "blur";
	var _source			= null;

	/**
	 * Mixin method added to our source html element to allow
	 * the element to add references to associated validator
	 * instances.
	 * 
	 * @param {Object} objValidator
	 * @param {Object} strTriggerEvent
	 */	
	var __addValidator = function(objValidator, strTriggerEvent) {
		if (this.validators == null) {
			this.validators = [objValidator];
		} else {
			this.removeValidator(objValidator);
			this.validators.push(objValidator);
		}						
	}


	/**
	 * Mixin method added to our source html element to allow
	 * the element to remove references to associated validator
	 * instances.
	 * 
	 * @param {Object} objValidator
	 * @param {Object} strTriggerEvent
	 */	
	var __removeValidator = function(objValidator) {
		if (this.validators) {
			for (var i = 0; i < this.validators.length; i++) {
				if (this.validators[i] == objValidator) {
					this.validators.splice(i, 1);
					return true;
				}
			}
		}
		return false;
	}

	var that = jsapp.extend(jsapp.event.EventDispatcher(), {
		property: 			'value',	
		requiredMessage:	'This is a required field',
		required:			true,
		enabled:			true,
		subFields:			[],
		
		/**
		 * Get the HTML element that this validator instance 
		 * is assigned to
		 */		
		getSource: function() {
			return _source;
		},
		
		/**
		 * Associates the Validator instance with an HTML element
		 * to validate. The source element is used in conjunction 
		 * with the property member variable when determining the
	 	 * value to validate.
	 	 * 
	 	 * @param {Object} source
	 	 */
		setSource:function(source) {	
			this.removeListenerHandler();
			this.removeTriggerHandler();
							
			_source = source;

			if (source) {
				this.initSourceElement(source);
				this.addListenerHandler();
				this.addTriggerHandler();
			}	else {

			}
		},	

		/**
		 * Get the HTML element that will trigger the Validator instance
		 */
		getTrigger:function() {
			return _trigger;
		},	

		/**
		 * Set the HTML element that will trigger the Validator instance
		 * 
		 * @param {Object} objTrigger
		 */
		setTrigger:function(objTrigger) {			
			this.removeTriggerHandler();
			_trigger = objTrigger;
			this.addTriggerHandler();
		},				

		/**
		 * Get the name of the event that will trigger the Validator 
		 * instance. The default value returned is 'blur'
		 */
		getTriggerEvent:function() {
			return _triggerEvent;
		},

		/**
		 * Set the name of the event that will trigger the Validator
		 * instance.
		 * 
		 * @param {Object} strTriggerEvent
		 */
		setTriggerEvent:function(strTriggerEvent) {
			this.removeTriggerHandler();	
			_triggerEvent = strTriggerEvent;
			this.addTriggerHandler();
		},

		/**
		 * Execute the Validator
		 * 
		 * @param {Object} value
		 * @param {Object} suppressEvents
		 */
		validate:function(value, suppressEvents) {

			if (value == null) {
				value = this.getValueFromSource();
			}
			
			if (this.isRealValue(value) || this.required) {
				return this.processValidation(value, suppressEvents);
			} else {
				return jsapp.validation.ValidationResultEvent(jsapp.validation.ValidationResultEvent.VALID, this.getSource());
			}
		},
				
		processValidation:function(value, suppressEvents) {
		
			var errorResults    = this.doValidation(value);
			var validationEvent = this.handleValidationResults(errorResults);			
			
			if (!this.enabled) {
				suppressEvents = true;
			}
			
			if (suppressEvents != true) {
				this.dispatchEvent(validationEvent);
			}
			
			return validationEvent;
		},

		/**
		 * Performs the actual validation logic. Custom Validator types
		 * should override this method with their own validation logic
		 * 
		 * @param {Object} value
		 */
		doValidation:function(value) {
			var results = [];
			var requiredResult = this.validateRequired(value);

			if (requiredResult) {
				results.push(requiredResult);
			}
			
			return results;
		},

		/**		
		 * returns a ValidationResultEvent. Override this if a custom
		 * validator needs to do some very specialised parsing of 
		 * validation results.
		 */ 
		handleValidationResults:function(errorResults) {
			if (errorResults.length > 0) {
				var event = jsapp.validation.ValidationResultEvent(jsapp.validation.ValidationResultEvent.INVALID, this.getSource());
				
				event.results = errorResults;
				
				// If we have any subfields, our doValidation method may not
				// have initialised any ValidationResult instances for subfields
				// that actually passed validation. We need to add these fields
				// to our results array so that these subfields can be updated
				// to reflect successful validation status.
				if (this.subFields.length > 0) {
					var errorSubFields = {};
					
					for (var i = 0; i < event.results.length; i++) {
						if (event.results[i].isError) {
							errorSubFields[event.results[i].subField] = true;
						}
					}
					
					for (var i = 0; i < this.subFields.length; i++) {
						if (!errorSubFields[this.subFields[i]]) {
							event.results.push(jsapp.validation.ValidationResult(false, this.subFields[i], ''));
						}
					}
				}
				
				return event;
			} else {
				return jsapp.validation.ValidationResultEvent(jsapp.validation.ValidationResultEvent.VALID, this.getSource());
			}
		},


		/**
		 * Determins whether a value is 'real' and should therefore be
		 * validated.
		 * 
		 * @param {Object} value
		 */
		isRealValue:function(value) {
			return (value != null);
		},

		/**
		 * Validates the presence of a required value. Custom Validator
		 * classes should call this method during their doValidation method
		 * if their required property is set to true.
		 * 
		 * @param {Object} value
		 */
		validateRequired:function(value) {
			if (value == '') {
				return jsapp.validation.ValidationResult(true, '', this.requiredMessage);
			}
			
			return null;
		},

		/**
		 * Gets the actual object that will trigger the Validator. This
		 * will either be the object set via a call to setTrigger or, by
		 * default, the source HTML element.
		 */
		getActualTrigger:function() {
			if (_trigger) {
				return _trigger;
			}

			if (_source) {
				return _source;
			}

			return null;
		},

		/**
		 * Gets the actual listeners of the Validator. This includes the source
		 * element and any object assigned as a listener via the setListener 
		 * method.
		 */
		getActualListeners:function() {
			// By default, our source element will
			// always be listening for validation
			// results
			var result = [];

			if (_source) {
				result.push(_source);
			}

			return result;
		},
	
		/**
		 * Gets the actual value to be validated from our source object.
		 */
		getValueFromSource:function() {
			if (this.getSource() != null) {
				return this.getSource()[this.property];
			}
		},
		
		/**
		 * Adds the valid and invalid event handlers to our source
		 * element. Specialised validators can override this to provide
		 * custom behaviours, such as updating multiple field appearances
		 * etc...
		 * 
		 * @param {Object} sourceElement
		 */
		initSourceElement:function(sourceElement) {
	
			sourceElement.validationResultHandler = function(validationResultEvent) {
				var el 		= validationResultEvent.element;
				var errorEl = document.getElementById(el.id + 'Error');

				if (validationResultEvent.results.length > 0) {
					errorEl.innerHTML = validationResultEvent.getMessage();

					if (el.className != '' && el.className != null) {
						el.className += ' ' + 'error';
					} else {
						el.className = 'error';
					}
				} else {
					errorEl.innerHTML = validationResultEvent.getMessage();

					if (el.className != '' && el.className != null) {
						var classes = el.className.split(' ');
						var newClasses = [];
						for (var i = 0; i < classes.length; i++) {
							if (classes[i] != 'error') {
								newClasses.push(classes[i]);
							}
						}
		
						el.className = newClasses.join(' ');
					}
				}			
			}	
		},
			
		addTriggerHandler:function() {
			var actualTrigger = this.getActualTrigger();

			if (actualTrigger) {
				// Add the addValidator and removeValidator
				// methods to the trigger object
				actualTrigger.addValidator    = __addValidator;
				actualTrigger.removeValidator = __removeValidator;
				actualTrigger.addValidator(this, _triggerEvent);

				if (actualTrigger.attachEvent) {
					actualTrigger.attachEvent('on' + _triggerEvent, this.triggerHandler);
				} else {
					actualTrigger.addEventListener(_triggerEvent, this.triggerHandler, false);
				}
			}
		},

		removeTriggerHandler:function() {
			var actualTrigger = this.getActualTrigger();

			if (actualTrigger) {
				actualTrigger.removeValidator(this, _triggerEvent);

				if (actualTrigger.detachEvent) {
					actualTrigger.detachEvent('on' + _triggerEvent, this.triggerHandler);
				} else {
					actualTrigger.removeEventListener(_triggerEvent, this.triggerHandler, false);
				}
			}
		},

		addListenerHandler:function() {
			if (_source) {
				this.addEventListener(jsapp.validation.ValidationResultEvent.VALID, _source, 'validationResultHandler');
				this.addEventListener(jsapp.validation.ValidationResultEvent.INVALID, _source, 'validationResultHandler');
			}			
		},

		removeListenerHandler:function() {
			if (_source) {
				this.removeEventListener(jsapp.validation.ValidationResultEvent.VALID, _source, 'validationResultHandler');
				this.removeEventListener(jsapp.validation.ValidationResultEvent.INVALID, _source, 'validationResultHandler');
			}
		},

		/**
		 * Runs within the context of our trigger element, therefore is in a different scope...
		 * @param {Object} e
		 */
		triggerHandler:function(e) {

			var target;

			if (!e) {
				var e = window.event;
			}

			if (e.target) {
				target = e.target;
			} else if (e.srcElement) {
				target = e.srcElement;
			} 

			if (target.nodeType == 3) { // defeat Safari bug
				target = target.parentNode;
			}

			if (target.validators != null) {
				for (var i = 0; i < target.validators.length; i++) {
					if (target.validators[i].getTriggerEvent() == e.type) {
						target.validators[i].validate();
					}
				}
			}
		}		
	});
	
	if (strElementId) {
		that.setSource(document.getElementById(strElementId));
	}
	
	return that;
}

jsapp.validation.ValidationResultEvent = function(type, element) {
	var event = {type:type, element:element, results:[]};
	event.getMessage = function() {
		var str = '';
		for (var i = 0; i < this.results.length; i++) {
			if (this.results[i].errorMessage) {
				if (i > 0) {
					str += ' ';
				}
				str += this.results[i].errorMessage;
			}
		}
		return str;
	}
	return event;
}

jsapp.validation.ValidationResultEvent.VALID   = 'valid';
jsapp.validation.ValidationResultEvent.INVALID = 'invalid';

jsapp.validation.ValidationResult = function(boolIsError, strSubField, strErrorMessage) {
	return {isError:boolIsError, subField:strSubField, errorMessage:strErrorMessage};
}
