/**
 * NS base
 *
 * @version   1.00.090812
 * @author    LBI Lost Boys
 */
(function($){

	var TYPE_FUNCTION = 'function';
	var TYPE_STRING = 'string';

	var PREF_REPLACE_INPUTS = 'replaceInputs';
	var PREF_SIMULATE_CLICK = 'simulateClick';
			
	var DEBUG = false; 

	/**
	 * Global controller, manages all applications and components. On domready the 
	 * initialize method is called, which sets up essential functionality for all
	 * pages. Then, a global "initialize" event is fired for external apps and/or
	 * components. 
	 */
	window.NS = function(runnable){
		switch (typeof runnable) {
			case TYPE_FUNCTION:
				return runnable.call(NS, $);
			case TYPE_STRING:
				NS.log(runnable);
			break;
		}
	};

	$.extend(NS, {
		applications: {},
		initialize:function() {
			// tell css we have javascript
			$('body').addClass('js-enabled');
			
			// route clicks to the dispatcher
			NS.Dispatcher.capture('click' /*, document */);

			// create custom click events on links only
			NS.Dispatcher.createEvent('click:link', function(e, data) {
				var link = $(e.target).closest('a');
				return link[0] || null;
			});
			
			// create custom click events on inputs only
			NS.Dispatcher.createEvent('click:input', function(e, data) {
				var input = $(e.target).closest('input,select,textarea');
				return input[0] || null;
			});
						
			// initalialize essential objects
			this.linkRelations  = new LinkRelations();
			this.inputRelations = new InputRelations();
			this.statistics     = new Statistics();

			// fire initialize event, applications listen for this event
			NS.Dispatcher.fire('initialize');
			
			// initialize objects that may depend on application preferences
			this.autocomplete  = NS.AjaxWrapper(new NS.Autocomplete());
			this.forms = NS.AjaxWrapper(new Forms());
		},

		/**
		 * Add, get and find allow objects and applications to communicate
		 */
		addApplication:function(name, application){
			this.applications[name] = application;
			NS.log('added application: ' + name);
		},

		getApplication:function(name){
			return this.applications[name] || null;
		},

		findApplication:function(form){
			for(var i in this.applications) {
				if(typeof i == TYPE_STRING) {
					var app = this.applications[i];
					if(app.owns && app.owns(form)) {
						return app;
					}
				}
			}
			
			// The NS object serves as a fallback for preferences
			return this;
		},

		getLanguage:function() {
			if(!this.language) {
				var equiv = /msie\s?6/i.test(navigator.userAgent)? 'httpequiv' : 'http-equiv';
				this.language = $('head meta[' + equiv + '="content-language"]').attr('content');
			}
			return this.language;
		},

		setCookie:function(name, value, expires){
			var date = new Date();
			date.setTime(date.getTime() + ((expires || 14) * 86400000));
			document.cookie = name + "=" + encodeURIComponent(value) + "; expires=" + date.toUTCString() + "; path=/";
		},

		getCookie:function(name) {
			var reg = new RegExp(name+'=([^;$]+)', 'i');
			var cookie = reg.exec(document.cookie);
			return (cookie && cookie[1])? decodeURIComponent(cookie[1]) : '';
		},

		// returns a property from config, or from a specified form
		getProperty:function(name, form) {
			var input = form && form.elements[name];
			if(input && input.value) {
				return input.value;
			}

			return Globals[name];
		},

		trim:function(value) {
			return value.replace(/(^\s*)|(\s*$)/mig, '');
		},

		log:function() {
			if(DEBUG) {
				try {
					console.log.apply(console, arguments);
				} catch(e) {
				}
			}
		},

		exit:function() {
			NS.Dispatcher.fire('exit');
		},

		/**
		 * Application fallback
		 */
		Defaults: {
			validateRequired: true,
			validateServer: false,
			replaceInputs: true,
			simulateClick: true
		},

		owns: function(form){ 
			return false; 
		},

		prefers: function(setting) { 
			return this.Defaults[setting];
		},
		
		/**
		 *  "facades"
		 */
		subscribe: function(type, handler, priority) {
			NS.Dispatcher.subscribe(type, handler, priority);
		},

		unsubscribe: function(type, handler, priority) {
			NS.Dispatcher.unsubscribe(type, handler, priority);
		},
			
		relateLink:function(reg, handle, owner) {
			this.linkRelations.subscribe(reg, handle, owner);
		},

		relateInput:function(reg, handle, owner) {
			this.inputRelations.subscribe(reg, handle, owner);
		},		
		
		getFormValues:function(form) {
			return this.forms.getValues(form);
		}
	});
	
	
	/**
	 * PUBLIC components
	 *
	 * The components below, both static instances as well as usable classes, are added to
	 * the NS namespace to allow usage by external apps and components.
	 */

	/**
	 * Class provides simple OO functionality
	 */
	NS.Class = {
		extend:function(Base, constructor, prototype) {
			var Extended = function() {
				Base.apply(this, arguments);
				constructor.apply(this, arguments);
			};

			this.implement(Extended, Base.prototype);
			if(prototype) {
				this.implement(Extended, prototype);
			}
			return Extended;
		},

		implement:function(Class, protoface) {
			for(var i in protoface) {
				if(typeof i === TYPE_STRING) {
					Class.prototype[i] = protoface[i];
				}
			}
		}
	};

	
	/**
	 * The dispatcher allows objects to observe and fire native and custom events. Typically,
	 * multiple objects observe an event, which is fired by only a few (or one) objects.
	 * The Dispatcher is essentially the main trafficer for everything that is going on.
	 */
	NS.Dispatcher = {
		events: {},
		custom: {},
		
		// captures native events on nodes or document, and routes them to the dispatcher
		capture:function(event, nodes) {
			$(nodes || document).bind(event, function(e, data){
				return NS.Dispatcher.fire(event, e.target, data, e);
			});
		},
		
		// creates custom events on top of native events, eg click:link. The eventFilter
		// method must return either a node if the event may be fired, or null if not.
		// These events can be extended again and again, eg click:link:hash.
		createEvent:function(customEvent, eventFilter) {
			var baseEvent = /(.*):[a-z0-9]+$/i.exec(customEvent)[1];
			this.custom[customEvent] = eventFilter;
			NS.Dispatcher.subscribe(baseEvent, function(e) {
				var target = eventFilter(e, e.data);
				if(target && target.nodeType) {
					return NS.Dispatcher.fire(customEvent, target, e.data, e);
				}
			});
		},
		
		// subscribe to any event
		subscribe:function(type, handler, priority){
			// custom types that extend a native event must exist, or they won't get fired.
			if(/:/.test(type) && !this.custom[type]) {
				throw Error('Custom event "'+type+'" is not defined.');
			}
			
			// if not present, create a list for the given type
			if(!this.events[type]) {
				this.events[type] = [];
			}
			
			// priority defines at which point a handler is inserted
			var events = this.events[type];
			for(var i=0; i<events.length; i++) {
				if(priority > events[i].priority) { break; }
			}

			events.splice(i, 0, {handler: handler, priority: priority || 1});
		},

		// unsubscribe from an event
		unsubscribe:function(type, handler /*, priority*/){
			var events = this.events[type] || [];
			for(var i=0; i<events.length; i++) {
				var e = events[i];
				if(e.handler == handler) {
					events.splice(i, 1);
				}
			}
		},
		
		// fire any event
		fire:function(type, target, data, nativeEvent){
			var events = this.events[type];
			if(events && events.length) {
				var e = new NS.Event(type, target, data, nativeEvent);
				for(var i=0; i<events.length; i++) {
					events[i].handler(e);
				}
				
				if(DEBUG) {
					NS.log("fired " + type+" for " + events.length + " handlers ", events);
				}
				
				return e.returnValue;
			}
			return true;
		}
	};

	/**
	 * Fireable event, used by the dispatcher as an argument to individual handlers.
	 * Handlers may use the preventDefault method to cancel (stoppable) events.
	 */
	NS.Event = function(type, target, data, e) {
		this.type = type;
		this.target = target;
		this.data = data || {};
		this.event = e;
		this.returnValue = true;
		
		if(e) {
			var list = NS.Event.Whitelist;
			for(var prop in list) {
				if(typeof prop === TYPE_STRING && list[prop]) {
					this.setProperty(prop, e[prop]);
				}
			}
		}
	};

	NS.Event.prototype = {
		preventDefault:function() {
			if(this.event) {
				this.event.preventDefault();
			}

			this.returnValue = false;
		},

		stopPropagation:function() {
			if(this.event) {
				this.event.stopPropagation();
			}
		},

		setProperty:function(name, value) {
			this[name] = value;
		}
	};

	// whitelist of event properties that may be copied to the wrapper
	NS.Event.Whitelist = {
		// targets and codes
		relatedTarget: 1,
		hashTarget: 1,
		button:   1,
		keyCode:  1,
		
		// keys
		ctrlKey:  1,
		shiftKey: 1,
		altKey:   1,
		
		// coordinates
		pageX:    1,
		pageY:    1,
		screenX:  1,
		screenY:  1
	};

	/**
	 * The DOM mediator ensures that the proper events are dispatched when the DOM is
	 * changed via ajax. Objects that subscribed to these events may then react to it.
	 * DOM events cannot be prevented by observers.
	 */
	NS.DOM = {
		write:function(node, html){
			$(node).html(html);
			NS.Dispatcher.fire('DOMNodeInserted', node);
		},
		
		append:function(node, fragment){
			$(node).append(fragment);
			NS.Dispatcher.fire('DOMNodeInserted', fragment);
		},

		replace:function(node, old) {
			NS.Dispatcher.fire('DOMNodeRemoved', old);
			$(old).replaceWith(node);
			NS.Dispatcher.fire('DOMNodeInserted', node);
		},

		remove:function(node){
			NS.Dispatcher.fire('DOMNodeRemoved', node);
			$(node).remove();
		}
	};

	/**
	 * The XHR mediator dispatches custom events for any observer that wishes to
	 * subscribe and/or react to them. XHR events may be prevented by observers.
	 * A validator may for instance prevent the submit, if the input is invalid.
	 */
	NS.XHR = {
		sendAndLoad:function(doc, url, func, type) {
			return NS.XMLHttp.sendAndLoad(doc, url, func, type);
		},

		sendForm:function(form, url, handler){
			var valid = NS.Dispatcher.fire('ajaxsubmit', form);
			if(valid) {
				var values = NS.getFormValues(form);
				var action = url || form.getAttribute('action');
				var method = form.getAttribute('method');

				return NS.XMLHttp.sendAndLoad(values, action, handler /*, method */ );
			}
			return valid;
		},

		load:function(url, handler){
			return NS.XMLHttp.load(url, handler);
		},
		
		abort:function(xhr) {
			NS.XMLHttp.abort(xhr);
			NS.Dispatcher.fire('ajaxabort');
		}
	};

	/**
	 * The AjaxWrapper serves as a wrapper for objects we want to inform
	 * of the DOMNodeInserted event.
	 */
	NS.AjaxWrapper = function(object) {
		NS.Dispatcher.subscribe('DOMNodeInserted', function(e){
			object.parseNode(e.target);
		});
		return object;
	};


	/**
	 * PRIVATE components
	 *
	 * The components below are private to this file, and provide functionality that is either
	 * handled automatically, or that is made public via another way, for instance a public call
	 * on the NS namespace.
	 */


	/**
	 * LinkRelations handles document wide clicks on links, and relays them
	 * to a given application when the observer's expression matches the rel
	 * attribute value.
	 */
	function LinkRelations() {
		this.relations = [];
		NS.Dispatcher.subscribe('click:link', this.handleClick.bind(this));
	}

	LinkRelations.prototype = {
		subscribe:function(expression, handler, owner) {
			// subscribers are ordered owned first, then non-owned ones.
			for(var i=0; i<this.relations.length; i++) {
				if(owner && !this.relations[i].owner) { break; }
			}

			this.relations.splice(i, 0, {
				expression: expression,
				handler: handler,
				owner: owner
			});
		},

		handleClick:function(e) {
			var link = e.target;
			var rel = link.rel;
			if(!rel) { return; }

			var form = $(link).parents('form')[0];
			var app = form && NS.findApplication(form);

			for(var i=0; i<this.relations.length; i++) {
				var relation = this.relations[i];
				var owner = relation.owner;
				if(
					(!owner || owner == app) &&
					relation.expression.test(rel)
				){
					var prevent = relation.handler(link, rel);
					if(prevent) {
						e.preventDefault();
					}
				}
			}
		}
	};

	/**
	 * InputRelations monitors change events and relays them to a given
	 * application when the observer's expression matches the input's id.
	 */
	function InputRelations() {
		this.relations = [];
		NS.Dispatcher.subscribe('click:input', this.handleChange.bind(this));
	}

	InputRelations.prototype = {
		// subscribers are ordered owned first, then non-owned ones.
		subscribe:function(expression, handler, owner) {
			for(var i=0; i<this.relations.length; i++) {
				if(owner && !this.relations[i].owner) { break; }
			}

			this.relations.splice(i, 0, {
				expression: expression,
				handler: handler,
				owner: owner
			});
		},

		handleChange:function(e) {
			var input = e.target;
			var app = NS.findApplication(input.form);

			for(var i=0; i<this.relations.length; i++) {
				var relation = this.relations[i];
				var owner = relation.owner;
				if(
					(!owner || owner == app) &&
					relation.expression.test(input.id)
				){
					relation.handler(input, input.id);
				}
			}
		}
	};

	/**
	 * Statistics mediator, passes clicks and submits to the siteStat object
	 */
	function Statistics() {
		NS.subscribe('click:link', this.handleClick.bind(this));
		NS.subscribe('submit', this.handleSubmit.bind(this));
	}

	Statistics.prototype = {
		handleClick:function(e) {
			var link = e.target, stat = link.getAttribute('ns:sitestat');
			if(stat) {
				window.siteStat.pageView.apply(window.siteStat, stat.split(';'));
			}
		},

		handleSubmit:function(e) {
			var form = e.target, stat = form.getAttribute('ns:sitestat');
			if(stat) {
				window.siteStat.trackForm(form, stat);
			}
		}
	};

	/**
	 * The Forms object monitors forms and binds global events.
	 */
	function Forms() {
		this.parseNode(document);
	}

	Forms.prototype = {
		parseNode:function(root) {
			this.bindFormEvents(root);
			this.replaceInputs(root);
		},

		bindFormEvents:function(root) {
			var inputs = $('input:text,input:radio,input:checkbox,select,textarea', root);
			var forms = $('form', root);

			NS.Dispatcher.capture('change', inputs);
			NS.Dispatcher.capture('submit', forms);

			// a hidden "js-action" input overrides the static action, used for non-js vs. js posts.
			forms.each(function(){
				var action = this.elements['js-action'];
				if(action && action.value) {
					this.setAttribute('action', action.value);
				}
			});

			// a hidden jsEnabled input may be used to inform the server of js support
			$('#jsEnabled', root).val('true');
		},

		// public submit helper for components that manually need to submit a form
		submit:function(form) {
			if(NS.Dispatcher.fire('submit', form)) {
				form.submit();
			}
		},

		// input replacer, triggers a click on the original input if preferred by a controlling app.
		replaceInputs:function(root) {
			var buttons = $('input[type].button'),
				template = '<a href="#" rel="$name" class="$className"><span>$value</span></a>';

			buttons.each(function(){
				var input = this,
					form = input.form,
					app = NS.findApplication(form);

				if(!app.prefers(PREF_REPLACE_INPUTS)) {
					return;
				}

				var jInput = $(input);
				var jButton = $(template.replace(/\$([a-z]+)/mig, function(match, attr){
					return input[attr] || '';
				}));
				
				jInput.removeClass('button');
				jInput.addClass('replaced');
				jInput.after(jButton);
				
				if(app.prefers(PREF_SIMULATE_CLICK)) {
					jButton.bind('click', function(e){
						// prevent default link action
						e.preventDefault();
						
						// fire the submit event; e.target is the form, e.data.explicitTarget the clicked input
						if($(form).triggerHandler('submit', {explicitTarget: input}) !== false) {
							// if none of the handlers returned false, fire a click on the original input.
							jInput.trigger('click');
						}
					});
				}
			});
		},

		// returns form input as a postable string
		getValues:function(form) {
			var element, type, post = '', 
				input = /(text|select)/i, 
				hidden = /hidden/i, 
				select = /select/i, 
				elements = form.elements || $('input,select,textarea', form);

			for(var i=0; i<elements.length; i++) {
				element = elements[i];
				type = element.type;
				if((input.test(type) && element.offsetHeight) || hidden.test(type) || element.checked) {
					if(select.test(element.nodeName)) {
						var index = element.selectedIndex;
						if(index >= 0 && element[index]) {
							var opt = element[index];
							post += element.name + '=' + encodeURIComponent(opt.value || opt.text) + '&';
						}
					} else {
						post += element.name + '=' + encodeURIComponent(element.value) + '&';
					}
				}
			}
			return post;
		}
	};

	/**
	 * Helpers
	 */
	Function.prototype.bind = function(scope) {
		var method = this;
		return function() {
			return method.apply(scope, arguments);
		};
	};

	/**
	 * Backward compatibility
	 */
	window.EventListener = {
		addEvent:function(node, type, handler) {
			$(node).bind(type, handler);
		}
	};

	/**
	 * Bind to domready and unload events
	 */
	$(document).ready(function(){
		NS.initialize();
	});

	$(window).bind('beforeunload', function(){
		NS.exit();
	});

})(jQuery);