/**
 * Refers to the global scope (typically the window-object)
 */
var global = this;

/**
 * Creates one or more nested objects that may be used as a namespace. Namespace objects are always
 * created in the global scope. 
 * @param ns {String} A string that specifies the namespace to be created. When creating multiple
 * nested namespaces, namespace components have to be separated with a dot. An exemplary namespace
 * string might be "com.mpnewmedia.cmex"
 * @return While the namespace object is automatically added to the global scope, the root object of
 * the namespace is also returned as an object.
 */
function namespace(ns) {
	var nsComponents = ns.split(/\./g);	
	var obj = global;
	for (var i = 0; i < nsComponents.length; i++) {
		if (obj[nsComponents[i]]) {
			obj = obj[nsComponents[i]];		
		} else {
			var nsObj = {};		
			obj[nsComponents[i]] = nsObj;
			obj = nsObj;
		}
	}
	return obj;
}

/**
 * Loops through the array and invokes the specified function for each element. If the function returns
 * false, the operation is aborted.
 * @param func {Function} The function that is invoked for each element. As its first parameter, the
 * function receives the current element. The function may return false to abort the operation.
 */
Array.prototype.each = function(func) {
	var length = this.length;
	for (var i = 0; i < length; i++) {
		if (func(this[i]) === false) {
			return;
		}	
	}
};

/**
 * Returns a new array that is the result of applying the specified function to each element in the
 * array. 
 * @param func {Function} The function that is invoked for each element. As its first parameter, the
 * function receives the current element. The return value of the function is then added to the new
 * array.
 * @return {Array} The new array.
 */
Array.prototype.map = function(func) {
	var result = [];
	var length = this.length;
	for (var i = 0; i < length; i++) {
		result.push(func(this[i]));	
	}
	return result;
};

/**
 * Returns a new array that is composed of only the elements in the existing array for which the 
 * specified function returns true.
 * @param func {Function} The function that is invoked for each element. As its first parameter
 * the function receives the current element. If the function returns true, the current element is
 * added to the new array, otherwise it is discarded.
 * @return {Array} The new array.
 */ 
Array.prototype.filter = function(func) {
	var result = [];
	var length = this.length;
	for (var i = 0; i < length; i++) {
		if (func(this[i])) {
			result.push(this[i]);	
		}
	}
	return result;	
};

/**
 * Returns the value that is the result of applying the specified function to each element in the
 * array. This function, among other things, may be used to calculate the sum of all elements in
 * an array. Example: var sum = myArray.reduce(function(old, element) { return old + element;}, 0);
 * This will calculate the sum of all elements in the array.
 * @param func {Function} The function that is used to reduce the array to a single value. As its
 * first parameter, the function receives the value produced by the previous call and as its second
 * parameter the current element.
 * @param initial {Object} The value that is passed to to the function specified in func when it is
 * called for the first time.
 * @return {Object} The value produced by invoking the reduce-function for each element.
 */
Array.prototype.reduce = function(func, initial) {
	var result = initial;
	var length = this.length;
	for (var i = 0; i < length; i++) {
		result = func(result, this[i]);
	}
	return result;		
};

/*
 * Utilities
 */
namespace("utils");

/**
 * This constuctor resolves a problem that occurs when invoking a member function from within another 
 * function that is not a member of the same object. It ensures that the this-variable inside that function
 * points to the correct object by "capturing" it inside a closure.
 * @constructor
 * @param obj {Object} The object of which the function is a member
 * @param method {Function} The function
 */
utils.Delegate = function(obj, method) {
	/**
	  * Executes the function in the given context. Any arguments passed to this function are passed
	  * on to the function managed by this object.
	  * @return The function's return value
	  */ 
	this.execute = function() {
		return method.call(obj, arguments);
	};
};

/**
 * A generic implementation of the observer pattern.
 * @constructor
 */
utils.EventDispatcher = function() {
	var listeners = {};
	
	/**
	  * Adds a new function to the list of observers.
	  * @param event {Object} The type of event that will be observed.
	  * @param listener {Function} The function or Delegate object that observers the event.
	  */
	this.addListener = function(event, listener) {
		if (!listeners[event]) {
			listeners[event] = [];	
		}
		listeners[event].push(listener);
	};
	
	/**
	 *  Removes a function from the list observers.
	 * @param event {Object}The type of even that should no longer be observed.
	 * @param listener {Function} The function or Delegate object that should be removed from the list 
	 * of observers.
	 */
	this.removeListener = function(event, listener) {
		if (!listeners[event]) {
			return;	
		}
		var list = listeners[event];
		for (var i = 0; i < list.length; i++) {
			if (list[i] == listener) {
				list = list.splice(i, 1);
				return;
			}
		}		
	};
	
	/**
	  * Notifies the registered observers that a particular event has occured.
	  * @param {Object} event Type of the even that has occurred.
	  */
	this.dispatchEvent = function(event) {
		if (!listeners[event]) {
			return;	
		}		
		var list = listeners[event];
		var args = [];
        var i;
		for (i = 1; i < arguments.length; i++) {
			args.push(arguments[i]);
		}
		for (i = 0; i < list.length; i++) {
			var listener = list[i];
			if (typeof listener == "function") {
				listener.call(this, args);	
			} else if (typeof listener == "object" && listener instanceof utils.Delegate) {
				listener.execute.call(this, args);	
			}
		}
	};
};

/**
 * Enables the interaction with and the observation of the browser history.
 * @constructor
 */
utils.History = function() {
	var eventDispatcher = new utils.EventDispatcher();
	
	var dummy = document.createElement("div"); 
	dummy.style.position = "absolute";
	dummy.style.visibility = "hidden";
	document.body.appendChild(dummy);
	var currentHash = window.location.hash;

	var timer = window.setInterval(function() {
		if (window.location.hash != currentHash) {
			currentHash = window.location.hash;
			eventDispatcher.dispatchEvent(0, currentHash.substring(1));
		}
	}, 100);

	/**
	 * Adds a new element to the browser history.
	 * @param page  {String} Unique identifier of the new element.
	 */
	this.add = function(page) {
		dummy.id = page;
		window.location.hash = page;  
		currentHash = "#" + page; 
	};

	/**
	 * Adds a new function to the list observers. This function is invoked whenever the user moves back and forth in  the browser history.
	 * @param callBack {Function} The function or Delegate object that is to observe changes in the browser history. As its first parameter, 
	 * the function receives the name of the history element as specified via the add method.
	 */
	this.addListener = function(callBack) {
		eventDispatcher.addListener(0, callBack);
	};

	/**
	 * Removes a function from the list of observers.
	 * @param callBack {Function} The function or Delegate that should no longer observe changes in the browser history.
	 */
	this.removeListener = function(callBack) {
		eventDispatcher.removeListener(0, callBack);
	};
};

/*
 * DOM
 */
namespace("dom");

/**
 * Cross-browser implementation of the DOM event model.
 */
dom.Event = (function() {
	var handlers = [];

	// Stops the propagation of events in Internet Explorer
	function stopPropagation() {
		this.cancelBubble = true;	
	}

	// Ensure that event objects share the same set of basic attributes
	// and methods across browsers
	function normalizeEventObject(e) {
		if (!e.target) {
			e.target = e.srcElement;	
		} 
		if (e.wheelDelta && !e.detail) {
			e.detail = e.wheelDelta;	
		} 
		if (e.clientX && !e.pageX) {
			e.pageX = e.clientX + document.documentElement.scrollLeft;
			e.pageY = e.clientY + document.documentElement.scrollTop;	
		}
		if (!e.stopPropagation) {
			e.stopPropagation = stopPropagation;
		}
		return e;
	}
	
	// Normalizes event names that differ between browsers
	function normalizeEventName(name) {
		if (document.addEventListener) {
			if (name == "mousewheel") {
				return "DOMMouseScroll";	
			}
		}
		return name;
	}

	// Ensures that event handlers are invoked with the event object as their
	// first parameter
	function createDelegate(obj, handler) {
		return function() {
			handler.call(obj, normalizeEventObject(window.event));
		};
	}
	
	var singleton = {
		/**
		* Registers an event listener with the specified object.
		 * @member dom.Event 
		 * @param obj {HTMLElement} The HTML element that is the even target.
		 * @param event {String} The name of the event to observe. Event names do not include the "on" prefix
		 * used in the HTML event model.
		 * @param handler {Function} The function that will be invoked whenever the event is triggered. This
		 * function receives as its first parameter a DOM event object. Note that within the event handler function,
		 * the this-variable will refer to the object that triggered the element rather than the object that the function
		 * is a member of.
		 */
		addListener: function(obj, event, handler) {
			event = normalizeEventName(event);
			if (obj.addEventListener) {
				obj.addEventListener(event, handler, false);
			} else if (obj.attachEvent) {
				var delegate = createDelegate(obj, handler);
				obj.attachEvent("on" + event, delegate);
				handler = delegate;
			}
			handlers.push([obj, event, handler]);
		},
		
		/**
		 * Removes an event listener from the specified object.
		 * @member dom.Event 
		 * @param obj {HTMLElement} The HTML element that is the event arget.
		 * @param event {String} The name of the event.
		 * @param handler {Function} The event handler function.
		 */
		removeListener: function(obj, event, handler) {
			event = normalizeEventName(event);
			if (obj.removeEventListener) {
				obj.removeEventListener(event, handler, false);
			} else if (obj.detachEvent) {
				obj.detachEvent("on" + event, handler);
			}
		}
	};
	
	// Prevent memory leaks on IE 6 by removing all event listeners once the page
	// is unloaded
	singleton.addListener(window, "unload", function() {
		for (var i = 0; i < handlers.length; i++) {
			var handler = handlers[i];
			singleton.removeListener(handler[0], handler[1], handler[2]);
		}
		singleton.removeListener(window, "unload", arguments.callee);
	});
	
	return singleton;
})();

/**
 * Convenience function that returns an HTML element by its ID.
 * @param id {String} ID of the HTML element to return.
 * @return The HTML element with the specified ID or null if no such element exists.
 */
dom.$ = function(id) {
	return document.getElementById(id);
};

/**
 * Convenience function that returns a list of HTML elements by their tag names.
 * @param tagName {String} The tag name of the HTML elements to return or "*" if all
 * HTML elements in the DOM tree should be returned.
 * @return {NodeList} A NodeList containing all HTMLElements in the document that match the given
 * criteria.
 */
dom.$$ = function(tagName) {
	return document.getElementsByTagName(tagName);
};

/**
 * Creates an XML document instance from the specified string.
 * @param {String} xmlString The string that contains the XML document.
 * @return {Document} A DOM document node.
 */
dom.documentFromString = function(xmlString) {
	if (typeof DOMParser != "undefined") {
		var parser = new DOMParser();
		return parser.parseFromString(xmlString, "text/xml");
	} else if (window.ActiveXObject) {
		var doc = new ActiveXObject("Microsoft.XMLDOM");
		doc.async = false;
		doc.loadXML(xmlString);
		return doc;
	}
    return null;
};

/**
 * Retrieves the opacity value for the specified HTML element.
 * @param {HTMLElement} element The HTML element the opacity will be retrieved from.
 * @return {Number} The opacity value in the range of 0 and 1.
 */
dom.getOpacity = function(element) {
	if (typeof element.style.opacity != "undefined" && element.style.opacity.length > 0) {
		return element.style.opacity;
	} else if (typeof element.style.MozOpacity != "undefined" && element.style.MozOpacity.length > 0) {
		return element.style.MozOpacity;
	} else if (typeof element.style.filter != "undefined" && element.style.filter.length > 0) {
		var regExp = /alpha\s?\(opacity\s?=\s?([0-9]+)/g;
		if (regExp.exec(element.style.filter)) {
			return RegExp.$1 / 100;
		}		
	}
	return 1;
};

/**
 * Sets the opacity value for the specified HTML element.
 * @param {HTMLElement} element The HTML element for which to set the opacity value.
 * @param {Number} opacity The new opacity value in the range of 0 and 1.
 */
dom.setOpacity = function(element, opacity) {
	var opacityPercent = opacity * 100.0;
	element.style.opacity = opacity;
	element.style.MozOpacity = opacity;
	element.style.filter = "alpha(opacity=" + opacityPercent + ")";
};

/**
 * Assigns a new or additional CSS-class to the specified HTML element. If the specified
 * class has already been specified for the given HTML element, no changes are made to
 * the element.
 * @param {HTMLElement} element The HTML element.
 * @param {String} className The CSS class name.
 */
dom.addClass = function(element, className) {
	var classes = element.className.split(/ /);
	var length = classes.length;
	for (var i = 0; i < length; i++) {
		if (classes[i] == className) {
			return;	
		}
	}
	classes.push(className);
	element.className = classes.join(" ");
};

/**
 * Removes the specified class from the HTML element's class attribute.
 * @param {HTMLElement} element The HTML element.
 * @param {String} className The CSS class to remove.
 */
dom.removeClass = function(element, className) {
	var classes = element.className.split(/ /);
	var newClasses = [];
	var length = classes.length;
	for (var i = 0; i < length; i++) {
		if (classes[i] != className) {
			newClasses.push(classes[i]);
		}
	}
	element.className = newClasses.join(" ");
};

/**
 * Hides the specified HTML element.
 * @param {HTMLElement} element The element to hide.
 */
dom.hide = function(element) {
	element.style.display = "none";
};

/**
 * Shows an HTML element that has previously been hidden using the dom.hide 
 * function.
 * @param {HTMLElement} element The element to show.
 */
dom.show = function(element) {
	element.style.display = "block";
};

/**
 * Removes all child nodes from the specified HTML element.
 * @param {HTMLElement} element The element that will have its child nodes removed. 
 */
dom.empty = function(element) {
	while (element.childNodes.length > 0) {
		element.removeChild(element.firstChild);	
	}
};

/**
 * Retrieves the specified CSS attribute from the given element, taking into account both
 * inline-styles and styles specified on a global level.
 * @param {HTMLElement} element The HTML element.
 * @param {String} attribute The CSS attribute to retrieve.
 * @return {String} The value of the specified attribute.
 */
dom.getStyle = function(element, attribute){
	if (document.defaultView && document.defaultView.getComputedStyle) {
		return document.defaultView.getComputedStyle(element, "").getPropertyValue(attribute);
	} else if (element.currentStyle) {
		attribute = attribute.replace(/\-(\w)/g, function(string, matched){
			return matched.toUpperCase();
		});
		return element.currentStyle[attribute];
	}
	return "";
};

/**
 * Provides a convenient way of creating HTML elements "on the fly". To create an element, simply type "dom."
 * followed by the lower-case tag-name of the element you wish to create. All elements specified by the XHTML
 * 1.0 standard are supported. The HTML-builder interface provides a function for each HTML element that can
 * be invoked with a series of parameters. When passing a string to an element function, a text node will be 
 * acreated and appended to the HTML element. Additionally, an object may be passed to the function that can 
 * be used to initialize the element's attributes. Calls to the element functions can nested to create
 * arbitrarily complex element hierarchies. The return value of each element function is the newly created
 * HTML element. Example: var element = dom.a({href: "http://test.de"}, dom.strong("Hello World")); This would
 * produce the following HTML document: <a href="http://test.de"><strong>Hello World</strong></a>.
 */
(function() {
	var tags = ['a', 'abbr', 'acronym', 'address', 'area', 'b', 'base', 'bdo', 'big', 'blockquote', 'body',
		'br', 'button', 'caption', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'div', 'dfn', 'dl', 'dt',
		'em', 'fieldset', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'hr', 
		'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'map', 'meta',
		'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'p', 'param', 'pre', 'q', 'samp',
		'script', 'select', 'small', 'span', 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea',
		'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'ul', 'var'];
	
	function createElement(tag) {
		var argumentCount = arguments.length;

		var element;
        var argument;
        var key;
		// Work-around for IEs name-attribute bug
		var isIE = !window.opera && navigator.userAgent.indexOf('Internet Explorer') > -1;
		if (isIE) {
			var name = false;
			for (var i = 1; i < argumentCount; i++) {
				argument = arguments[i];
				if (typeof argument == "object" && argument != null && !argument.nodeName) {
					for (key in argument) {
						if (key == "name") {
							name = argument;	
						}
					}
				}
			}
			if (name !== false) {
				element = document.createElement('<' + tag + ' name="' + name + '"/>');	
			} else {
				element = document.createElement(tag);
			}
		} else {
			element = document.createElement(tag);	
		}
		
		for (i = 1; i < argumentCount; i++) {
			argument = arguments[i];
			if (typeof argument == "object" && argument != null) {
				if (argument.nodeName != null) {
					element.appendChild(argument);
				} else {
					for (key in argument) {
						switch (key) {
							case "style":	
								element.style.cssText = argument[key];
								break;
							case "disabled":
							case "checked":
							case "selected":
								if (argument[key] === true) {
									element.setAttribute(key, key);	
								}
								break;
							default:
								if (key.substr(0, 2) == "on" && typeof argument[key] == "function") {
									element[key] = argument[key];	
								} else {
									element.setAttribute(key, argument[key]);	
								}
						}
					}
				}
			} else {				
				element.appendChild(document.createTextNode(argument));					
			}
		}
		
		return element;
	}
	
	for (var tag in tags) {
		dom[tags[tag]] = (function(tag) {
			return function() {
				var args = [tag];
				for (var i = 0; i < arguments.length; i++) {
					args.push(arguments[i]);
				}
				return createElement.apply(this, args);	
			};	
		})(tags[tag]);
	}
})();
