"use strict";

var Events = (function () 
{
	/**
	 * workaround for Array.indexOf() (as seen from the loop, it actually is
	 * for Array.lastIndexOf(), though that is only to return -1 if not found)
	 *
	 * @param (mixed) val          value to search for
	 * @param (Array) arr          array to search in
	 * @return (int)               array index of val or -1
	 */
	function array_search(val, arr) 
	{
		if (Array.indexOf) {
			return arr.indexOf(val);
		}
		else {
			var i = arr.length;
			while (i--) { 
				if (arr[i] && arr[i] === val) {
					break;
				} 
			}
			return i;
		}
	}

	/**
	 * workaround for Function.call()
	 *
	 * @param (Function) Fn        function reference
	 * @param (Node)               node on which the handler is to execute
	 * @param (Event)              current event object
	 * @return (mixed)             return value of the function Fn
	 */
	function callHandler(Fn, obj, evt)
	{
		var retValue = true;
		
		if (Function.call) {
			retValue = Fn.call(obj, evt);
		} 
		else {
			try { 
				obj.__fn = Fn;
				retValue = obj.__fn(evt);
				delete obj.__fn; 
			} 
			catch (f) { 
				obj.__fn = null; 
			}
		}
		return retValue;
	}

	/**
	 * build the array of elements to loop through in capturing phase
	 *
	 * @param (Node) elem          element from which to search up
	 * @return (Array)             list of elements starting with input
	 *                             element until top node
	 */
	function getParents(elem)
	{
		var p = [elem];
		while (elem.parentNode) {
			elem = elem.parentNode;
			p.push(elem);
		}
		return p;
	}
	
	/**
	 * replacement for Event.stopPropagation()
	 *
	 * @return (void)
	 */
	function stopEvent()
	{
		this.cancelEvent  = true;
		this.cancelBubble = true;
	}

	/**
	 * replacement for Event.preventDefault()
	 *
	 * @return (void)
	 */
	function prevent()
	{
		this.returnValue  = false;
	}

	/**
	 * function that executes events according to DOM
	 *
	 * @param (mixed) e            Event object, if passed
	 * @return (bool)              combined return value of bubble handlers
	 */
	function IEHandler(e)
	{
		// if no Event object is passed (IE)
		e = e || window.event;
		// add .stopPropagation()
		if (!e.stopPropagation) {
			e.stopPropagation = stopEvent;
		}
		// add .preventDefault()
		if (!e.preventDefault && e.returnValue) {
			e.preventDefault = prevent;
		}
		    // get Event Target
		var base     = e.target || e.srcElement,
			// get Event Type
			evType   = e.type, 
			// return values for bubbling handlers
			retValue = true, 
			i, j, l, elem, chain, evPhase;
		
		// execute only once
		if (base == this) {
			// get elements for capturing
			chain = getParents(base);
			for (i = chain.length; i--;) {
				elem = chain[i];
				// if there are functions attached to execute in capturing context
				if (elem.__events && elem.__events[evType] && elem.__events[evType].capture) {
					evPhase = elem.__events[evType].capture;
					// execute each function
					for (j = 0, l = evPhase.length; j < l; j++) {
						if (evPhase[j]) {
							// exit on stopPropagation()
							if (e.cancelEvent === true) {
								return null;
							}
							// there can be no value returned due to the executing element of this loop is the Event target and not the element the handler executes on.
							callHandler(evPhase[j], elem, e);
						}
					}
				}
			}
		}
		// if there are functions attached to execute in bubbling context
		if (this.__events && this.__events[evType] && this.__events[evType].bubble) {
			evPhase = this.__events[evType].bubble;
			for (j = 0, l = evPhase.length; j < l; j++) {
				if (evPhase[j]) {
					// return type is Boolean, though other return types could be possible
					retValue = !!callHandler(evPhase[j], this, e) && retValue;
				}
			}
		}
		return retValue;
	}

	/**
	 * adding an event handler to an Element
	 *
	 * @param (mixed*) obj         element to attach the handler to
	 * @param (string) evType      Event name (W3C, i.e. without "on")
	 * @param (Function) fn        handler function to attach
	 * @param (bool) useCapture    Event Phase modifyer
	 * @return (bool)              handler attach success
	 *
	 * [* - W3C compliant systems implement EventTarget, if it is not 
	 *      supported, obj must implement the Element interface]
	 */
	function addListener(obj, evType, fn, useCapture)
	{
		// W3C
		if (obj.addEventListener) {
			obj.addEventListener(evType, fn, !!useCapture);
			return true;
		} 
		else if (3 === obj.nodeType) {
			var evtObj, phase = useCapture ? "capture" : "bubble";
			
			// create __events property
			if (!obj.__events) {
				 obj.__events = {};
			}
			// create __events.eventType property
			if (!obj.__events[evType]) {
				 obj.__events[evType] = {};
			}
			// shortcut reference
			evtObj = obj.__events[evType];
			// search in __events.eventType.bubble / __events.eventType.capture
			// if function is already registered
			if (evtObj[phase]) {
				if (array_search(fn, evtObj[phase]) > -1) {
					return false;
				}
			}
			// create ".__events.eventType.bubble"/".__events.eventType.capture" property
			else {
				evtObj[phase] = [];
				if (!useCapture && obj['on' + evType] && obj['on' + evType] != IEHandler) {
			//	if (!useCapture && obj['on' + evType]) {
					// add any previous function assigned to .onevent
					evtObj[phase][0] = obj['on' + evType];
				}
			}
			// add function to stack
			evtObj[phase].push(fn);
			// set "global" handler
			obj['on' + evType] = IEHandler;
		//	obj.attachEvent('on' + evType, IEHandler.bind(obj));
			
			return true;
		}
		return false;
	}
	
	/**
	 * remove an event handler from an object. currently no browser implements
	 * DOM-3 EventTarget.
	 *
	 * @param (mixed*) obj         element to remove the handler from
	 * @param (string) evType      Event name (W3C, i.e. without "on")
	 * @param (Function) fn        handler function to remove
	 * @param (bool) useCapture    Event Phase modifyer
	 * @return (void)
	 *
	 * [* - cf. addListener()]
	 */
	function delListener(obj, evType, fn, useCapture)
	{
		if (obj.removeEventListener) {
			obj.removeEventListener(evType, fn, useCapture);
		} 
		else {
			var evTypeRef = '__' + evType,
				phase     = useCapture ? "capture" : "bubble",
				evPhase, i;
			
			if (obj[evTypeRef] && obj[evTypeRef][phase]) {
				evPhase = obj[evTypeRef][phase];
				i = array_search(fn, evPhase);
				if (i > -1) {
					try {
						delete evPhase[i];
					} 
					catch (e) {
						evPhase[i] = null;
					}
				}
			}
		}
	}

	/**
	 * return interface
	 *
	 * @public add
	 * @public remove
	 */
	return {
		/**
		 * add an event handler to an object or objects inside a list/Array.
		 *
		 * @param (mixed) obj          element/element items to attach the 
		 *                             handler to
		 * @param (string) evType      Event name (W3C, i.e. without "on")
		 * @param (Function) fn        handler function to remove
		 * @param (bool) useCapture    Event Phase modifyer
		 * @return (bool)              handler attach success
		 * @throws (TypeError)         handler is not a function
		 */
		add : function (obj, evType, fn, useCapture) 
		{
			if (typeof fn != "function") {
				throw new TypeError("Function expected!");
			}
			// make useCapture a Boolean
			useCapture = !!useCapture;
			// try as Element
			var ret1 = addListener(obj, evType, fn, useCapture), 
			    ret2 = true, 
			    i    = obj.length;
			// try as NodeList/Array
			if (i && !ret1) {
				while (i--) {
					ret2 = addListener(obj[i], evType, fn, useCapture) && ret2;
				}
				return ret2;
			}
			return ret1;
		},

		/**
		 * remove an event handler to an object or objects inside a list/Array.
		 *
		 * @param (mixed) obj          element/element items to attach the 
		 *                             handler to
		 * @param (string) evType      Event name (W3C, i.e. without "on")
		 * @param (Function) fn        handler function to remove
		 * @param (bool) useCapture    Event Phase modifyer
		 * @return (void)
		 */
		remove : function (obj, evType, fn, useCapture) 
		{
			// make useCapture a Boolean
			useCapture = !!useCapture;
			// try as Element
			delListener(obj, evType, fn, useCapture);
			// try as NodeList/Array
			for (var i = obj.length; i--;) {
				delListener(obj, evType, fn, useCapture);
			}
		},
		
		loading : function (fn)
		{
			if (typeof fn != "function") {
				throw new TypeError("Function expected!");
			}
			// Mozilla, Opera, Webkit
			else if (document.addEventListener) {
				document.addEventListener("DOMContentLoaded", fn, true);
			}
			// IE
			else if (document.attachEvent) {
				document.attachEvent("onreadystatechange", function () {
					if (document.readyState == "complete") {
						fn();
					}
				});
			}
		}
	}
})();		

