/* Silently create the mcd namespace if it does not exist */
if (typeof mcd === 'undefined') {
	var mcd = {};
}

/**
 * mcd-js DOM Utilities
 * 
 * FYI: This is a module. http://yuiblog.com/blog/2007/06/12/module-pattern/
 * 
 * @author Michael Girouard (mgirouard@mcdpartners.com)
 */
mcd.dom = function () {
	
	/**
	 * Private member declarations
	 * @private
	 */
	var _this = {
		util : {
			trimStr : function (str) {
				return str.replace(/(^\s+|\s+$)/g, '');
			},

			/**
			 * Wrapper Function for handling inserting element based on having a string
			 * type parameter for atPositon
			 *
			 * @param {Object}
			 * @return {HTMLElement}
			 */
			handleString : function(parameters) {
				if (parameters['atPosition'] === 'after') {
					return this.handleInsertAfter(parameters['referenceElement'], parameters['newElement']);
				}
				else if (parameters['atPosition'] === 'before') {
					return this.handleInsertBefore(parameters['referenceElement'], parameters['newElement']);
				}
			},

			/**
			 * Inserts element after another element
			 *
			 * @param {Object}
			 * @return {HTMLElement}
			 */
			handleInsertAfter : function (referenceElement, newElement) {
				var nextElement = mcd.dom.getNextElement(referenceElement);

				if (nextElement) {
					return nextElement.parentNode.insertBefore(newElement, nextElement);
				}
				else {
					return referenceElement.parentNode.appendChild(newElement);
				}			
			},

			/**
			 * Inserts element before another element
			 *
			 * @param {Object}
			 * @return {HTMLElement}
			 */
			handleInsertBefore : function (referenceElement, newElement) {
				return referenceElement.parentNode.insertBefore(newElement, referenceElement);
			},

			/**
			 * Wrapper Function for handling inserting element based on having a number
			 * type parameter for atPositon
			 *
			 * @param {Object}
			 * @return {HTMLElement}
			 */
			handleNumber : function (parameters) {
				return this.handleInsertAtPosition(parameters['newElement'], parameters['atPosition'], parameters['in']);
			},

			/**
			 * Inserts element before at a position within a container
			 *
			 * @param {Object}
			 * @return {HTMLElement}
			 */
			handleInsertAtPosition : function (newElement, position, container) {
				var childElementArray = mcd.dom.getChildElements(container);

				if (position === childElementArray.length) {
					return container.appendChild(newElement);
				}
				else {
					return this.handleInsertAfter(childElementArray[position], newElement);
				}
			},

			/**
			 * NodeType Constants
			 */
			ELEMENT_NODE                : 1,
			ATTRIBUTE_NODE              : 2,
			TEXT_NODE                   : 3,
		    CDATA_SECTION_NODE          : 4,
		    ENTITY_REFERENCE_NODE       : 5,
		    ENTITY_NODE                 : 6,
		    PROCESSING_INSTRUCTION_NODE : 7,
		    COMMENT_NODE                : 8,
		    DOCUMENT_NODE               : 9,
		    DOCUMENT_TYPE_NODE          : 10,
		    DOCUMENT_FRAGMENT_NODE      : 11,
		    NOTATION_NODE               : 12
		},
		
		/**
		 * DOM ready settings
		 * 
		 * @private
		 */
		ready : {
			
			/**
			 * is ready
			 */
			isReady : false,
			
			/**
			 * Timer for WebKit polling
			 * 
			 * @private
			 */
			timer : null,
			
			/**
			 * Callback queue for DOM ready methods
			 * 
			 * @private
			 */
			queue : [],
			
			/**
			 * Runs all callbacks in the queue
			 * 
			 * This method should be explicitly executed by call() in order to
			 * resolve scope issues.
			 * 
			 * @private
			 */
			queueRunner : function () {
				this.isReady = true;
				
				if (this.timer) {
					clearInterval(this.timer);
				}
		
				for (var i = 0; i < this.queue.length; i++) {
					this.queue[i]();
				}
			},
			
			/**
			 * DOM ready initializer
			 * 
			 * @private
			 */
			init : function () {
				/* WebKit (safari, etc) */
				if (/WebKit/i.test(navigator.userAgent)) {
					_this.ready.timer = setInterval(function () {
						if (/loaded|complete/.test(document.readyState)) {
							_this.ready.queueRunner.call(_this.ready);
						}
					}, 10);
				}
		
				/* IE */
				else if ('all' in document && 'ActiveXObject' in window) {
					document.write('<script id="__ie_onload" defer src="//:"><\/script>');
					document.getElementById('__ie_onload').onreadystatechange = function () {
						if (this.readyState === 'complete') {
							_this.ready.queueRunner.call(_this.ready);
						}
					};
				}
		
				/* W3 DOM support */
				else if ('addEventListener' in document) {
					document.addEventListener('DOMContentLoaded', function () {
						_this.ready.queueRunner.call(_this.ready);
					}, false);
				}
		
				/* Everyone else */
				else {
					var oldUnload = window.onload || function () {};
					window.onload = function () {
						oldUnload();
						_this.ready.queueRunner.call(_this.ready);
					}
				}
			}
		}
	};
	
	/* Call the initializer */
	_this.ready.init();
	
	/* Public methods */
	return {
		
		/**
		 * Calls the provided method when the DOM is complete
		 * 
		 * @param {Function} callback
		 */
		ready : function (callback) {
			if (_this.ready.isReady) {
				window.setTimeout(function(){
					callback();
				}, 0);
			}
			else if (typeof callback === 'function') {
				_this.ready.queue.push(callback);
			}
		},
		
		/**
		 * Returns an element reference
		 * 
		 * @param  {String|HTMLElement} element
		 * @return {HTMLElement}
		 */
		getElement : function (element) {
			if (typeof element === 'string') {
				element = document.getElementById(element);
			}
			
			return element;
		},
		
		/**
		 * Locates nodes by their attribute values
		 * 
		 * @param  {String} attribute
		 * @param  {String} value
		 * @param  {String|HTMLElement} parent
		 * @param  {String} nodeName
		 * @return {Array}
		 */
		getElementsByAttribute : function (attribute, value, parent, nodeName, looseMatch) {
			parent     = this.getElement(parent || document);
			nodeName   = nodeName || '*';
			looseMatch = !!looseMatch;
		
			var out  = [];
			var nodeList = parent.getElementsByTagName(nodeName);
						
			for (var i = 0; i < nodeList.length; i++) {			
				if (attribute === 'class') {
					var attributeValue = nodeList[i].className;
				}
				else {
					var attributeValue = nodeList[i].getAttribute(attribute);
				}
				
				if(!attributeValue) continue; 
				
				if (looseMatch) {
					var isAMatch = (attributeValue.indexOf(value) > -1);
				}
				else {
					var isAMatch = (attributeValue === value);
				}
				
				if (isAMatch) {
					out.push(nodeList[i]);
				}
			}
			
			return out;
		},


		/**
		 * Locates nodes based on having attributes
		 * 
		 * @param  {String} attribute
		 * @param  {String|HTMLElement} parent
		 * @param  {String} nodeName
		 * @return {Array}
		 */
		getElementsHaveAttribute : function (attribute, parent, nodeName) {
			parent     = this.getElement(parent || document);
			nodeName   = nodeName || '*';
			
			var out  = [];
			var nodeList = parent.getElementsByTagName(nodeName);
			
			for (var i = 0; i < nodeList.length; i++) {
				var hasAttribute = new String(nodeList[i].getAttribute(attribute));
				if (!!hasAttribute && hasAttribute.length > 0) {
					out.push(nodeList[i]);
				}
			}
			
			return out;
		},

		/**
		 * Returns the next sibling element node
		 *
		 * @param {HTMLElement}
		 * @return {HTMLElement||False}
		 */
		getNextElement : function (currentElement) {
			element = currentElement.nextSibling;

			while (element) {
				if (element.nodeType !== 1) {
					element = element.nextSibling;
				}
				else {
					return element;
				}
			}

			return false;
		},

		/**
		 * Returns the previous sibling element node
		 *
		 * @param {HTMLElement}
		 * @return {HTMLElement||FALSE}
		 */
		getPreviousElement : function (currentElement) {
			element = currentElement.previousSibling;

			while (element) {
				if (element.nodeType !== ELEMENT_NODE) {
					element = element.previousSibling;
				}
				else {
					return element;
				}
			}

			return false;
		},

		/**
		 * Returns all child elements of a container
		 *
		 * @param {HTMLElement}
		 * @return {Array}
		 */
		getChildElements : function (element) {
			var childElements = [];
			var childNodes = element.childNodes;

			for (var i = 0; i < childNodes.length; i++) {
				if (childNodes[i].nodeType === _this.util.ELEMENT_NODE) {
					childElements.push(childNodes[i]);
				}
			}

			return childElements;
		},

		/**
		 * Returns the X,Y coordinates of an element
		 * 
		 * Originally developed by PPK: http://quirksmode.org/js/findpos.html
		 * 
		 * @param  {String|HTMLElement}
		 * @return {Array}
		 */
		getPosition : function (element) {
			element = this.getElement(element);
	
			var curleft = 0;
			var curtop  = 0;
	
			if(element == null ||  element=="") return false;
			if (element.offsetParent) {
				do {
					curleft += element.offsetLeft;
					curtop  += element.offsetTop;
				} 
				while (element = element.offsetParent);
			}
	
			return [ curleft, curtop ];
		},
		
		/**
		 * Sets the position of an element
		 * 
		 * This requires the element to be absolutely positioned. This can be 
		 * forced by passing 'true' to the third parameter.
		 * 
		 * @param  {String|HTMLElement}
		 * @param  {Array} position
		 * @param  {Boolean} forcePosition (optional)
		 * @return {HTMLElement}
		 */
		setPosition : function (element, position, forcePosition) {
			element = this.getElement(element);
			
			if (forcePosition) {
				element.style.position = 'absolute';
			}
			
			element.style.left = position[0] + 'px';
			element.style.top  = position[1] + 'px';
			
			//if (element.style.setAttribute) {
                		//element.style.setAttribute ('left',position[0] + 'px','important');
                		//element.style.setAttribute ('top',position[1] + 'px','important');
            		//} 
            		//else {
                		//element.style.setProperty ('left',position[0] + 'px','important');
                		//element.style.setProperty ('top',position[1] + 'px','important');
            		//}
			
			return element;
		},
		
		/**
		 * Tests if an element has a class applied
		 * 
		 * @param  {String|HTMLElement} element
		 * @param  {String} className
		 * @return {Boolean}
		 */
		hasClass : function (element, className) {
			return !!this.getElement(element).className.match(
				new RegExp('(\\s|^)' + className + '(\\s|$)')
			);
		},
		
		/**
		 * Adds a class to an element
		 * 
		 * @param  {String|HTMLElement} element
		 * @param  {String} className
		 * @return {Bool} True if the class was applied, False otherwise.
		 */
		addClass : function (element, className) {
			element = this.getElement(element);
			
     	                if(element == null ||  element=="") return false;
			if (!this.hasClass(element, className)) {
				element.className += ' ' + className;
				element.className = _this.util.trimStr(element.className);
				return true;
			}
			else {
				return false;
			}
		},
		
		/**
		 * Removes a class from an element
		 * 
		 * @param  {String|HTMLElement} element
		 * @param  {String} className
		 * @return {Bool} True if the class was removed, False otherwise.
		 */
		removeClass : function (element, className) {
			element = this.getElement(element);
			
			  if(element == null ||  element=="") return false;
			if (this.hasClass(element, className)) {
				element.className = element.className.replace(
					new RegExp('(\\s|^)' + className + '(\\s|$)'), ' '
				);
				
				element.className = _this.util.trimStr(element.className);
				return true;
			}
			else {
				return false;
			}
		},
		
		/**
		 * Toggles a class on an element
		 * 
		 * @param {String|HTMLElement} element
		 * @param {String} className
		 */
		toggleClass : function (element, className) {
			if (!this.removeClass(element, className)) {
				this.addClass(element, className);
			}
		},
		
		/**
		 * Inserts Element
		 * 
		 * @param {Object} parameters
		 * @return {String} className
		 */
		insertElement : function (parameters) {
			switch (typeof parameters['atPosition']) {
				case 'string':
					return _this.util.handleString(parameters);
				break;
				
				case 'number':
					return _this.util.handleNumber(parameters);
				break;
				
				default:
					throw 'Invalid atPosition type';
				break;	
			}
		},
			
		/**
		 * Returns the document size
		 * 
		 * @return {Object} size The total size of the document as an array [X,Y]
		 */
		getDocumentSize : function () {
			var size = [mcd.dom.getDocumentWidth(), mcd.dom.getDocumentHeight()];
			
			return size;
		},
		
		/**
		 * Returns the document width
		 * 
		 * @return {Integer} size The total width of the document
		 */
		getDocumentWidth : function () {
			var scrollWidth = (document.compatMode != 'CSS1Compat') ? document.body.scrollWidth : document.documentElement.scrollWidth;
			
			return Math.max(scrollWidth, mcd.dom.getViewportWidth());
		},
		
		/**
		 * Returns the document height
		 * 
		 * @return {Integer} size The total height of the document
		 */
		getDocumentHeight : function () {
			var scrollHeight = (document.compatMode != 'CSS1Compat') ? document.body.scrollHeight : document.documentElement.scrollHeight;
			
			return Math.max(scrollHeight, mcd.dom.getViewportHeight());
		},
		
		/**
		 * Returns the viewport size
		 * 
		 * @return {Object} size The size of the viewport as an array [X,Y]
		 */
		getViewportSize : function () {
			var size = [mcd.dom.getViewportWidth(), mcd.dom.getViewportHeight()];
			
			return size;
		},	
		
		/**
		 * Returns the viewport's width
		 * 
		 * @see http://developer.yahoo.com/yui/docs/Dom.js.html
		 * @return {Integer} size The width of the viewport
		 */
		getViewportWidth : function () {
			var size = self.innerWidth; // Returns default size (Fall through for old versions of Safari)
			
			if (document.compatMode) {
				if (document.compatMode === 'CSS1Compat') {
					//Standards mode gets standard compliant code
					size = document.documentElement.clientWidth;
				}
				else {
					//Quirks mode gets quirks mode code
					size = document.body.clientWidth;
				}
			}
			
			return size;
		},
		
		/**
		 * Returns the viewport's height
		 * 
		 * @see http://developer.yahoo.com/yui/docs/Dom.js.html
		 * @return {Integer} size The height of the viewport
		 */
		getViewportHeight : function () {
			var size = self.innerHeight; // Returns default size (Safari's fall through)
			
			if (document.compatMode) {
				if (document.compatMode === 'CSS1Compat') {
					//Standards mode gets standard compliant code
					size = document.documentElement.clientHeight;
				}
				else {
					//Quirks mode gets quirks mode code
					size = document.body.clientHeight;
				}
			}
			
			return size;
		}
	}
	
}();