/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of "Scrivo" nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: browser.js 783 2013-08-09 10:17:59Z geert $
 */

"use strict";

/**
 * Holds a set of browser specific functions.
 * @namespace
 */
SUI.browser = {

	/**
	 * Is the code executed by a Mozilla type browser?
	 * @type boolean
	 */
	isGecko: false,

	/**
	 * Is the code executed by an IE type browser?
	 * @type boolean
	 */
	isIE: false,

	/**
	 * Is the code executed by an Opera type browser?
	 * @type boolean
	 */
	isOpera: false,

	/**
	 * Is the code executed by a WebKit type browser?
	 * @type boolean
	 */
	isWebKit: false,

	/**
	 * What's the version of the browser?
	 * TODO not really supported yet.
	 * @type boolean
	 */
	version: null,

	/**
	 * Some older browsers (notably IE 8) don't implement Array.indexOf. This
	 * is the fix for a missing Array.indexOf method as proposed on MDN.
	 */
	patchNoArrayIndexOf: function() {

		if (!Array.prototype.indexOf) {
			Array.prototype.indexOf = function(searchElement /*, fromIddx */) {
				"use strict";
				if (this === void 0 || this === null) {
					throw new TypeError();
				}
				var t = Object(this);
				var len = t.length >>> 0;
				if (len === 0) {
					return -1;
				}
				var n = 0;
				if (arguments.length > 0) {
					n = Number(arguments[1]);
					if (n !== n) {
						n = 0;
					} else if (n !== 0 && n !== (1 / 0) && n !== -(1 / 0)) {
						n = (n > 0 || -1) * Math.floor(Math.abs(n));
					}
				}
				if (n >= len) {
					return -1;
				}
				var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
				for (; k < len; k++) {
					if (k in t && t[k] === searchElement)
					return k;
				}
				return -1;
			};
		}

	},

	/**
	 * Fix for String.substr implementations (IE) that do not support a
	 * negative index.
	 */
	patchSubstrIE: function() {

		if (SUI.browser.isIE) {
			String.prototype.substr = function(off, len) {
				// make the implicit len parameter explicit
				if (!len) {
					len = this.length;
				}
				if (off < 0) {
					// if offset is negative work from other side of string ...
					off = this.length+off;
					// ... but correct if (abs) offset was larger than string
					if (off < 0) {
						off = 0;
					}
				}
				return this.substring(off, off+len);
			};
		}
	},

	/**
	 * Get the browser type and version. The function has no return values, but
	 * it sets the isIE, isGEcko, isOpera, isWebKit and version fields of the
	 * SUI.browser object.
	 * TODO version not really supported yet
	 */
	getBrowser: function() {

		var ua, s, i;
		// we'll use the user agent string to determine the browser
		ua = navigator.userAgent;

		if ((i = ua.indexOf("Opera")) >= 0) {

			// Opera first because it as also MSIE in the identification string
			this.isOpera = true;

		} else if ((i = ua.indexOf("MSIE")) >= 0) {

			this.isIE = true;
			this.version = parseFloat(ua.substr(i + 4));

		} else if ((i = ua.indexOf("WebKit")) >= 0) {

			this.isWebKit = true;

		} else {

			// Assume a gecko browser ??
			// TODO look into this
			this.isGecko = true;
			this.version = 6.1;

		}

	},

	/**
	 * Add an event listener to an HTML element node. This is basically a
	 * wrapper for HTMLElementNode.addEventListener() because we would like to
	 * use the standard mechamism but can't because of IE.
	 * Also note that we really can't 'add' eventlisteners because of IE,
	 * unless we're adding event listeners to the window object. That's
	 * because in this library we normally store the listener element in an
	 * SUI.Event object. Using attachEvent (what you want to use if you want
	 * to add listeners) will leave you with a this pointer that points to
	 * the window object instead of the element that handles te event.
	 * So except for the window object there is no posibility to add multiple
	 * event listners to a HTML element. IE will only recognize the last one
	 * added when using this function.
	 * @param {HTMLElementNode} el The HTML element node to add the event
	 *     listener to.
	 * @param {String} id An event id (like: "click", "mouseover", etc.).
	 * @param {Function} fn Listener function with signature ({DOMEvent}).
	 *     Note that in IE the listener will be called with no parameter and
	 *     that access to the DOMEvent will be provided by the widow object.
	 */
	addEventListener: function(el, id, fn) {
		if (this.isIE && this.version < 9) {
			// can't use attachEvent: loses el that has the event
			// listener (this)
			if (el == window || el.tagName == "IFRAME") {
				el.attachEvent("on"+id, fn);
			} else {
				// IE has no input event but propertychange works more or less
				// like it
				if (id == "input") {
					id = "propertychange";
				}
				el["on"+id] = fn;
			}
		} else {
			// the standard way, no capturing bubbling only
			el.addEventListener(id, fn, false);
		}
	},


	/**
	 * Remove an event listener from an HTML element node that was previously
	 * added with addEventListener. It is basically a wrapper for
	 * HTMLElementNode.removeEventListener() needed for IE compatibility.
	 * @param {HTMLElementNode} el The HTML element node to add the event
	 *     listener to.
	 * @param {String} id An event id (like: "click", "mouseover", etc.).
	 * @param {Function} fn A valid reference to the listener function that
	 *     earlier was added with addEventListener method.
	 */
	removeEventListener: function(el, id, fn) {
		if (this.isIE && this.version < 9) {
			// Only use detachEvent on the window object
			if (el == window) {
				el.detachEvent("on"+id, fn);
			} else {
				// IE has no input event but propertychange works more or less
				// like it
				if (id == "input") {
					id = "propertychange";
				}
				el["on"+id] = null;
			}
		} else {
			// the standard way, no capturing bubbling only
			el.removeEventListener(id, fn, false);
		}
	},

	/**
	 * Stop the propagation (bubbling) of the event upwards the DOM tree.
	 * This is basically a wrapper for DOMEvent.stopPropagation needed for
	 * IE compatibility.
	 */
	noPropagation: function(e) {
			// If the event was not passed (what will normally be the case in
			// IE) use the window.event. Is some circumstances we will use
			// contentWindow.event (iframe) and then we'll set e explictly.
			if (!e) {
				e = window.event;
			}
		if (this.isIE && this.version < 9) {
			// the MS way to stop propagation
			e.cancelBubble = true;
		} else if (e){
			// the standard way to stop propagation
			e.stopPropagation();
		}
	},

	/**
	 * Counter to create unique id's within the scope of the page.
	 * @type int
	 * @private
	 */
	_id: 1,

	/**
	 * Return a new unique id. Note that the id sequence is initialized when
	 * this module is loaded.
	 * @return {String} An unique id.
	 */
	newId: function() {
		return "scrivo-ui-"+this._id++;
	},

	/**
	 * <p>Create an HTML element with a set of default properties set. In the
	 * SUI library most elements are created by this function to ensure some
	 * standard settings. The following element properties are set:</p>
	 * <ul>
	 *    <li>not selectable unless it's a from element,</li>
	 *    <li>set a standard font (class),</li>
	 *    <li>disable system context menu's,</li>
	 *    <li>no margin or padding,</li>
	 *    <li>the default cursor,</li>
	 *    <li>absolute positioning</li>
	 *    <li>generate a unique id</li>
	 * </ul>
	 * @param {String} [tag="DIV"] HTML tag name of the element to create
	 * @param {Object} [attr] Object containing the name and value pairs
	 *    for the elements attributes.
	 */
	createElement: function(tag, attr) {

		// create an the element
		var div = document.createElement(tag ? tag : "DIV");

		// set the attributes as given in the arguments (setAttribute sets -
		// not adds - to the className so do it here)
		if (attr) {
			this.setAttributes(div, attr);
		}

		// set the font class
		SUI.style.addClass(div, "sui-font");

		// IE uses unselectable and it does not inherit so set the property
		// on all created elements.
		if (this.isIE) {
			div.unselectable = true;
		}

		// if we've created a div element ...
		if (!tag) {
			// ...set standard style for divs
			SUI.style.addClass(div, "no-select");
			div.style.overflow = "hidden";
		} else {
			// ... else was it an input field?
			if (tag == "INPUT" || tag == "TEXTAREA") {
				// ... set styles for input fields ...
				SUI.style.addClass(div, "select");
				// ... and make them selectable in IE
				if (this.isIE) {
					div.unselectable = false;
				}
			}
		}

		// disable the default context menu
		SUI.browser.addEventListener(div, "contextmenu", function(e) {
			if (SUI.browser.isIE) {
				window.event.returnValue = false;
			} else {
				e.preventDefault();
			}
		});

		// set the other styles
		div.style.position = "absolute";
		div.style.margin = "0px";
		div.style.padding = "0px";
		div.style.cursor = "default";

		// and generate a unique id for the element
		div.id = this.newId();

		return div;
	},

	/**
	 * Get the current/computed style of an HTML element. You can either
	 * retrieve the style object itself or a single property.
	 * Note: You can only use this function if the HTML content of the page
	 * is fully rendered because the currentStyle is asynchronious in IE.
	 * @param {HTMLElementNode} el HTML element node to get the computed
	 *     style (property) from.
	 * @param {String} [property] The style property to retrieve.
	 * @return {Object|*} The style object if no specific property was set,
	 *     or else the requested style property.
	 */
	currentStyle: function(el, property) {
		if (SUI.browser.isIE) {
			return property ? el.currentStyle[property] : el.currentStyle;
		} else {
			property = property
				? property.replace(/([A-Z])/g, "-$1").toLowerCase() : null;
			var st = el.ownerDocument.defaultView.getComputedStyle(el, null);
			return property ? st.getPropertyValue(property) : st;
		}
	},

	/**
	 * Add and overwrite the attributes of some HTML element node with the
	 * attributes of another.
	 * @param {HTMLElementNode} newMode The node to copy the attributes to.
	 * @param {HTMLElementNode} oldMode The node to copy the attributes from.
	 */
	mergeAttributes: function(newNode, oldNode) {
		if (SUI.browser.isIE) {
			newNode.mergeAttributes(oldNode);
		} else {
			for(var i=0; i<oldNode.attributes.length; i++) {
				var a = oldNode.attributes[i];
				if (a.name === "class" || a.name === "className")  {
					SUI.style.addClass(newNode.className, oldNode.className);
				} else {
					newNode.setAttribute(a.name, oldNode.getAttribute(a.name));
				}
			}
		}
	},

	/**
	 * Set/unset a number of attributes using an object. The attributes get
	 * the values from the corresonding properties. A property value of null
	 * will remove the attribute.
	 * @param {HTMLElementNode} e Element node of which the attributes to set.
	 * @param {Object} attr Object containing the new values for the
	 *    attributes.
	 */
	setAttributes: function(e, attr) {
		for (var i in attr) {
			if (attr.hasOwnProperty(i)) {
				if (i === "class" || i === "className") {
					SUI.style.setClass(e, attr[i]===null ? "" : attr[i]);
				} else {
					if (attr[i] === null) {
						e.removeAttribute(i);
					} else {
						e.setAttribute(i, attr[i]);
					}
				}
			}
		}
	},

	/**
	 * Remove an element from the document tree. Either remove the node
	 * completely or keep the nodes child nodes in place.
	 * @param {HTMLElementNode} e Element node to remove.
	 * @param {boolean} removeChildren Also remove the children of the node.
	 */
	removeNode: function(node, removeChildren) {
		if (node.removeNode) {
			// MS method
			return node.removeNode(removeChildren);
		} else {
			if (removeChildren) {
				// remove the node completely
				return node.parentNode.removeChild(node);
			} else {
				// replace the node with its contents
				var range = document.createRange();
				range.selectNodeContents(node);
				return node.parentNode.replaceChild(
					range.extractContents(), node);
			}
		}
	},

	/**
	 * The current width of the browser window (viewport).
	 * @type int
	 */
	viewportWidth: 0,

	/**
	 * The current height of the browser window (viewport).
	 * @type int
	 */
	viewportHeight: 0,

	/**
	 * Get the browser window (viewport) size and set the viewportWidth and
	 * viewportHeight members of the SUI.browser object. Note that this
	 * function returns no values.
	 */
	getVieportSize: function() {
		if(SUI.browser.isIE) {
			// standards compliant mode of IE
			this.viewportWidth = document.documentElement.clientWidth;
			this.viewportHeight = document.documentElement.clientHeight;
		} else {
			this.viewportWidth = window.innerWidth;
			this.viewportHeight = window.innerHeight;
		}
	},

	/**
	 * Get the vertical scroll offset of the viewport.
	 * @return {int} The vertical scroll offset of the viewport.
	 */
	viewportScrollX: function() {
		return SUI.browser.isIE ? document.documentElement.scrollLeft :
			window.scrollX;
	},

	/**
	 * Get the horizontal scroll offset of the viewport.
	 * @return {int} The horizontal scroll offset of the viewport.
	 */
	viewportScrollY: function() {
		return SUI.browser.isIE ? document.documentElement.scrollTop :
			window.scrollY;
	},

	/**
	 * Get the horizontal mouse position from a mouse event with respect to
	 * the left of the document.
	 * @param {DOMEvent} e A mouse event.
	 * @return {int} The distance from the left side of the document to the
	 *     mouse click in pixels.
	 */
	getX: function(e) {
		if (SUI.browser.isIE && !e) {
			// If the event was not passed (what will normally be the case in
			// IE) use the window.event. Is some circumstances we will use
			// contentWindow.event (iframe) and then we'll set e explictly.
			e = window.event;
		} 
		return e.clientX + this.viewportScrollX();
	},

	/**
	 * Get the vertical mouse position from a mouse event with respect to
	 * the top of the document.
	 * @param {DOMEvent} e A mouse event.
	 * @return {int} The distance from the top side of the document to the
	 *    mouse click in pixels.
	 */
	getY: function(e) {
		if (SUI.browser.isIE && !e) {
			// If the event was not passed (what will normally be the case in
			// IE) use the window.event. Is some circumstances we will use
			// contentWindow.event (iframe) and then we'll set e explictly.
			e = window.event;
		}
		return e.clientY + this.viewportScrollY();
	}

};