 /**
 * DOM
 * Provides various helper functions to ease access to and manipulation of the DOM.
 * @constructor
 */
function DOM()
{
	// Private
	var self		= this,
		elements	= {};

	// Public
	this.create				= create;
	this.createText			= createText;
	this.getById			= getById;
	this.getByClassName		= getByClassName;
	this.getFirstByTagName	= getFirstByTagName;
	this.getLastByTagName	= getLastByTagName;
	this.extend				= extend;

	// Constructor
	document.getById			= getById;
	document.getByClassName		= getByClassName;
	document.getFirstByTagName	= getFirstByTagName;
	document.getLastByTagName	= getLastByTagName;

	if (typeof document.addEventListener == "undefined")
	{
		document.addEventListener		= addEventListener;
		document.removeEventListener	= removeEventListener;

		window.addEventListener		= addEventListener;
		window.removeEventListener	= removeEventListener;
	}
		

	/**
	 * Creates a new DOM element.
	 * @param {String} elementName The name of the new element.
	 * @param {Object} [attributes] Attributes to be set to the element.
	 * @param {Array} [childNodes] Child 
	 * @throws {Invalid argument} elementName is not a string.
	 * @throws {Invalid argument} attributes is not an object.
	 * @throws {Invalid argument} childNodes is not an array.
	 * @return {Object} A DOM element.
	 */
	function create(elementName, attributes, childNodes)
	{
		if (typeof elementName != "string") 
		{
			throw new Error("Invalid argument: elementName is not a string");
		}

		var element = document.createElement(elementName);

		if (attributes != undefined)
		{
			if (typeof attributes == "object") 
			{
				for (var attribute in attributes) 
				{
					element[attribute] = attributes[attribute];
				}
			}
			else 
			{
				throw new Error("Invalid argument: attributes is not an object");
			}
		}

		if (childNodes != undefined)
		{
			if (childNodes.constructor == Array) 
			{
				for (var i = 0; childNodes[i] != undefined; i++) 
				{
					// The childNode is an element, append it "as is"
					if (childNodes[i].nodeType != undefined && childNodes[i].nodeType == 1) 
					{
						element.appendChild(childNodes[i]);
					}

					// Anything else is turned into a text node
					else 
					{
						element.appendChild(document.createTextNode(childNodes[i]));
					}
				}
			}
			else 
			{
				throw new Error("Invalid argument: childNodes is not an array");
			}
		}

		return extend(element);
	}

	/**
	 * Creates a new text node.
	 * @param {String} text The value of the text node.
	 * @return {Object} A text node.
	 */
	function createText(text)
	{
		return document.createTextNode(text);
	}

	/**
	 * Get an element from the DOM based on its id.
	 * @param {String} elementId The id of the requested element.
	 * @param {Boolean} overwriteCache Overwrite the internal cache, forcing a new lookup in the document.
	 * @throws {Invalid argument} elementId is missing or not a string.
	 * @return {Object} A DOM element.
	 */
	function getById(elementId, overwriteCache)
	{
		if (typeof elementId != "string") 
		{
			throw new Error("Invalid argument: elementId is missing or not a string");
		}

		// Remove item from cache if needed
		if (overwriteCache == true && elements[elementId] != undefined) 
		{
			delete elements[elementId];
		}

		// Check if the element is cached
		if (elements[elementId] != undefined) 
		{
			return elements[elementId];
		}

		var element;

		// Check if the element exists, cache it and return it
		if ((element = document.getElementById(elementId)) != null)
		{
			return elements[elementId] = extend(element);
		}

		return null;
	}

	/**
	 * Get elements based on their class.
	 * @param {String} className The class of the requested elements.
	 * @param {String} elementName The element name of the requested elements.
	 * @throws {Invalid argument} className is missing or not a string.
	 * @return {Array} An array containing DOM elements.
	 */
	function getByClassName(className, elementName)
	{
		if (typeof className != "string") 
		{
			throw new Error("Invalid argument: className is missing or not a string");
		}

		if (typeof elementName != "string") 
		{
			elementName = "*";
		}

		var element		= (this == self ? document : this),
			selection	= element.getElementsByTagName(elementName),
			elements	= [],
			regEx		= new RegExp("(^| )" + className + "($| )", "g");

		for (var i = 0; i < selection.length; i++)
		{
			if (regEx.test(selection[i].className) == true)
			{
				elements.push(extend(selection[i]));
			}
		}

		return elements;
	}

	/**
	 * Get the first element from the DOM with the specified tag name.
	 * @param {String} elementName The tag name of the requested element.
	 * @throws {Invalid argument} elementName is missing or not a string.
	 * @return {Object} A DOM element.
	 */
	function getFirstByTagName(elementName)
	{
		if (typeof elementName != "string") 
		{
			throw new Error("Invalid argument: elementName is missing or not a string");
		}

		// This function will be attached to a DOM node, so "this" refers to the node
		var element = (this == self ? document : this),
			returnElements;

		if ((returnElements = element.getElementsByTagName(elementName)).length > 0)
		{
			return extend(returnElements[0]);
		}

		return null;
	}

	/**
	 * Get the last element from the DOM with the specified tag name.
	 * @param {String} elementName The tag name of the requested element.
	 * @throws {Invalid argument} elementName is missing or not a string.
	 * @return {Object} A DOM element.
	 */
	function getLastByTagName(elementName)
	{
		if (typeof elementName != "string") 
		{
			throw new Error("Invalid argument: elementName is missing or not a string");
		}

		// This function will be attached to a DOM node, so "this" refers to the node
		var element = (this == self ? document : this),
			returnElements;

		if ((returnElements = element.getElementsByTagName(elementName)).length > 0)
		{
			return extend(returnElements[returnElements.length - 1]);
		}

		return null;
	}

	/**
	 * Remove the element from the DOM.
	 */
	function remove()
	{
		if (this.parentNode != undefined)
		{
			this.parentNode.removeChild(this);
		}
	}

	/**
	 * Extend an element with various helper functions.
	 * @param {Object} element A DOM element.
	 * @throws {Invalid argument} element is not a DOM node.
	 * @return {Object} The extended DOM element.
	 */
	function extend(element)
	{
		if (element == undefined || element.nodeType == undefined || element.nodeType != 1) 
		{
			throw new Error("Invalid argument: element is not a DOM node");
		}

		element.getById				= getById;
		element.getByClassName		= getByClassName;
		element.getFirstByTagName	= getFirstByTagName;
		element.getLastByTagName	= getLastByTagName;
		element.addClass			= addClass;
		element.removeClass			= removeClass;
		element.hasClass			= hasClass;
		element.getStyle			= getStyle;
		element.getPosition			= getPosition;
		element.remove				= remove;

		if (typeof element.addEventListener == "undefined")
		{
			element.addEventListener	= addEventListener;
			element.removeEventListener	= removeEventListener;
		}

		return element;
	}

	/**
	 * Add a class to an element.
	 * @param {String} className The name of the class to be added.
	 * @throws {Invalid argument} className is missing or not a string.
	 */
	function addClass(className)
	{
		if (typeof className != "string") 
		{
			throw new Error("Invalid argument: className is missing or not a string");
		}

		// This function will be attached to a DOM node, so "this" refers to the node
		var element = this;

		element.className += (element.className == "" ? "" : " ") + className;
	}

	/**
	 * Remove a class from an element.
	 * @param {String} className The name of the class to be removed.
	 * @throws {Invalid argument} className is missing or not a string.
	 */
	function removeClass(className)
	{
		if (typeof className != "string") 
		{
			throw new Error("Invalid argument: className is missing or not a string");
		}

		// This function will be attached to a DOM node, so "this" refers to the node
		var element = this,
			classRegExp = new RegExp(" ?" + className, "g");

		element.className = element.className.replace(classRegExp, "");
	}

	/**
	 * Check if an element has a certain class.
	 * @param {String} className The name of the class to be checked.
	 * @throws {Invalid argument} className is missing or not a string.
	 */
	function hasClass(className)
	{
		if (typeof className != "string") 
		{
			throw new Error("Invalid argument: className is missing or not a string");
		}

		// This function will be attached to a DOM node, so "this" refers to the node
		var element = this,
			classRegExp = new RegExp("(^| )" + className + "($| )", "g");

		return (element.className.match(classRegExp) == null ? false : true);
	}

	/**
	 * Get the current value of a style property.
	 * @param {String} property The name of the property.
	 * @throws {Invalid argument} className is missing or not a string.
	 * @return {String} The value of the property.
	 */
	function getStyle(property)
	{
		if (typeof property != "string") 
		{
			throw new Error("Invalid argument: property is missing or not a string");
		}

		// This function will be attached to a DOM node, so "this" refers to the node
		var element = this;

		if (element.currentStyle != undefined)
		{
			return element.currentStyle[property];
		}
		else if (window.getComputedStyle != undefined)
		{
			return document.defaultView.getComputedStyle(element, null)[property];
		}

		return element.style[property];
	}

	/**
	 * Get the absolute position of the element.
	 * @type int
	 * @return {Object} An object with an x and y property, containing the respective coordinates.
	 */
	function getPosition()
	{
		// This function will be attached to a DOM node, so "this" refers to the node
		var element = this,
			position = {
				x : 0,
				y : 0
			};

		if (element.offsetParent) {
			position.x = element.offsetLeft;
			position.y = element.offsetTop;

			while ((element = element.offsetParent)) {
				position.x += element.offsetLeft;
				position.y += element.offsetTop;
			}

			return position;
		}

		return null;
	}

	/**
	 * Add an event listener to the element.
	 * @param {String} type The type of the event to listen for.
	 * @param {Function} listener A reference to the function to be called when the event is dispatched.
	 * @param {Boolean} capture Call the event listener before the event is passed on to other targets.
	 */
	function addEventListener(type, listener, capture)
	{
		this.attachEvent("on" + type, listener);
	}

	/**
	 * Remove an event listener from the element.
	 * @param {String} type The type of the event to listen for.
	 * @param {Function} listener A reference to the function to be called when the event is dispatched.
	 * @param {Boolean} capture Call the event listener before the event is passed on to other targets.
	 */
	function removeEventListener(type, listener, capture)
	{
		this.detachEvent("on" + type, listener);
	}

	// Singleton
	return (DOM.instance != undefined ? DOM.instance : (DOM.instance = this));
}