/*
 * Copyright 2012 Amadeus s.a.s.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var Aria = require("../Aria");
require("../core/JsonValidator");
var ariaCoreBrowser = require("../core/Browser");
var ariaUtilsString = require("./String");
var ariaUtilsCssUnits = require("./css/Units");

/**
 * This class contains utilities to manipulate the DOM.
 */
module.exports = Aria.classDefinition({
    $classpath : "aria.utils.Dom",
    $singleton : true,
    $statics : {

        VIEWPORT : "__$viewport",

        DIV_NOT_FOUND : "Missing div '%1' in DOM.",
        INSERT_ADJACENT_INVALID_POSITION : "Invalid position %1. Expected one of: beforeBegin, afterBegin, beforeEnd or afterEnd.",

        pxRegExp : /^[0-9]+(\.[0-9]+)?px$/

    },
    $prototype : {

        /**
         * To be used instead of document.getElementById because IE7 does not retrieve correctly the elements in some
         * cases
         * @param {String} id the 'id' parameter of the element to find
         * @return {HTMLElement}
         */
        getElementById : function (id) {
            if (ariaCoreBrowser.isIE7) {
                this.getElementById = function (id) {
                    var document = Aria.$window.document;
                    var el = document.getElementById(id);
                    if (el) {
                        // If id match, return element
                        if (el.getAttribute("id") == id) {
                            return el;
                        } else {
                            for (var elem in document.all) {
                                if (elem.id == id) {
                                    return elem;
                                }
                            }
                        }
                    }
                    return null;
                };
            } else {
                this.getElementById = function (id) {
                    var document = Aria.$window.document;
                    return document.getElementById(id);
                };
            }
            return this.getElementById(id);
        },

        /**
         * Get one of the elements in the DOM which follows the given DOM element.
         * @param {HTMLElement} domElt reference DOM element
         * @param {Number} count [optional, default: 1], number of the following DOM element (1 for the immediately
         * following DOM element)
         * @return {HTMLElement}
         */
        getNextSiblingElement : function (domElt, count) {
            if (count == null) {
                count = 1;
            }
            while (domElt && count > 0) {
                domElt = domElt.nextSibling;
                if (domElt && domElt.nodeType == 1) {
                    count--;
                }
            }
            return domElt;
        },

        /**
         * Get one of the elements in the DOM which precedes the given DOM element.
         * @param {HTMLElement} domElt reference DOM element
         * @param {Number} count [optional, default: 1], number of the preceding DOM element (1 for the immediately
         * preceding DOM element)
         */
        getPreviousSiblingElement : function (domElt, count) {
            if (count == null) {
                count = 1;
            }
            while (domElt && count > 0) {
                domElt = domElt.previousSibling;
                if (domElt && domElt.nodeType == 1) {
                    count--;
                }
            }
            return domElt;
        },

        /**
         * Used to retrieve a node element of type ELEMENT
         * @param {HTMLElement} parentNode
         * @param {Integer} index the expected index in the list (0=first)
         * @param {Boolean} reverse true to start from the last child with reverse order
         * @return {HTMLElement} the dom elt or null if not found
         * @public
         */
        getDomElementChild : function (parentNode, index, reverse) {
            if (!parentNode) {
                return null;
            }
            var childNodes = parentNode.childNodes, count = 0, l = childNodes.length;
            for (var i = (reverse) ? l - 1 : 0; (reverse) ? i >= 0 : i < l; (reverse) ? i-- : i++) {
                if (childNodes[i].nodeType == 1) {
                    // this is an element
                    if (count == index) {
                        return childNodes[i];
                    }
                    count++;
                }
            }
            return null;
        },
        /**
         * Used to retrieve node elements with a specifig TAG name
         * @param {HTMLElement} parentNode The HTML node which descendants should be searched
         * @param {String} tagName Name of the tag, i.e. 'label'
         * @return {HTMLElement} An array of elements with specific tag name
         */
        getDomElementsChildByTagName : function (parentNode, tagName) {
            if (!parentNode) {
                return null;
            }
            return parentNode.getElementsByTagName(tagName);
        },

        /**
         * Used to retrieve a node element of type ELEMENT, with the index counting from the last child.
         * @param {HTMLElement} parentNode
         * @param {Integer} index the expected index, counting from the last one in the list (0=last)
         * @return {HTMLElement} the dom elt or null if not found
         */
        getDomElementChildReverse : function (parentNode, index) {
            return this.getDomElementChild(parentNode, index, true);
        },

        /**
         * This method, intended to be called after changes in the DOM, makes IE understand that it must update the
         * display (which it would do only after mouse move otherwise). It does nothing on other browsers.
         * @param {HTMLElement} domElt
         */
        refreshDomElt : function (domElt) {
            if (ariaCoreBrowser.isIE7) {
                this.refreshDomElt = function (domElt) {
                    // Ugly fix for IE, it might fail if domElt is inside an iFrame
                    try {
                        // PTR5975877 (IE7) PTR6034278 (IE9 in IE7mode)
                        var s1 = domElt.parentNode.style, s2 = domElt.style;
                        var dummyCss = "foo:foo;"; // do not add leading ';' here!
                        s1.cssText += "";
                        s1.zoom = 1;
                        s2.cssText += dummyCss;
                        s2.cssText = s2.cssText.replace(dummyCss, "");
                        // PTR06973328 (IE7)
                        s2.zoom = 1;
                    } catch (ex) {}
                };
            } else if (ariaCoreBrowser.isIE8) {
                this.refreshDomElt = function (domElt) {
                    // why on earth it is necessary to write code like this is a mystery
                    // but as it stands, we abide. fixes PTR 04273172
                    // refreshing only the parentNode className is not enough, see regression 04563420
                    // when you're dealing with shit, there's no limit to the smell
                    // -----------------------------------------------------------
                    // Solution of PTR 04563420: the div overlay after a refresh can be fixed by hiding and redesplaying
                    // the body like this:
                    // document.body.style.display = 'none';
                    // document.body.style.display = 'block';
                    // But this causes a loss of focus, for example on text inputs. Another solution (which is less
                    // efficient
                    // but does not compromise the focus) is the following:
                    for (var i = 0, l = Aria.rootTemplates.length; i < l; i++) {
                        var rootTemplate = Aria.rootTemplates[i];
                        if (rootTemplate._cfg && rootTemplate._cfg.tplDiv) {
                            var tplDiv = rootTemplate._cfg.tplDiv;
                            tplDiv.className = tplDiv.className;
                            tplDiv = null;
                        }
                    }
                };
            } else {
                this.refreshDomElt = function (domElt) {};
            }

            this.refreshDomElt(domElt);
        },

        /**
         * Replace the HTML contained in an element by another piece of HTML.
         * @param {aria.templates.CfgBeans:Div} idOrElt div whose content have to be replaced
         * @param {String} newHTML html to set
         * @return {Object} Reference to the html element, or null if the element was not found in the DOM. If the
         * element was not found in the DOM, the DIV_NOT_FOUND error is also logged. Note that in IE, in tables, the
         * returned domElt can be different from the one given as a parameter.
         */
        replaceHTML : function (idOrElt, newHTML) {

            // PROFILING // var msr1 = this.$startMeasure("ReplaceHTML");
            var domElt = idOrElt;
            if (typeof(domElt) == "string") {
                domElt = this.getElementById(domElt);
            }
            if (domElt) {
                if ((ariaCoreBrowser.isIE7 || ariaCoreBrowser.isIE8) && aria.utils && aria.utils.Delegate) {
                    try {
                        var activeElement = Aria.$window.document.activeElement;
                        if (activeElement && this.isAncestor(activeElement, domElt)) {
                            // On IE 7-8, there is an issue after removing from the DOM a focused element.
                            // We detect it here so that next time there is a need to focus an element, we focus the
                            // body first (which is the work-around for IE 7-8)
                            aria.utils.Delegate.ieRemovingFocusedElement();
                        }
                    } catch (e) {
                        // on (real) IE8, an "Unspecified error" can be raised when trying to read
                        // Aria.$window.document.activeElement
                        // It happens when executing the following test: test.aria.utils.cfgframe.AriaWindowTest
                        // It does not happen with IE 11 in IE8 mode.
                    }
                }
                // PROFILING // var msr2 = this.$startMeasure("RemoveHTML");
                // TODO: check HTML for security (no script ...)
                try {
                    domElt.innerHTML = ""; // this makes IE run faster (!)
                    // PROFILING // this.$stopMeasure(msr2);
                    domElt.innerHTML = newHTML;
                } catch (ex) {
                    // PTR 04429212:
                    // IE does not support setting the innerHTML property of tbody, tfoot, thead or tr elements
                    // directly. We use a simple work-around:
                    // create a complete table, get the HTML element and insert it in the DOM
                    var newDomElt = this._createTableElement(domElt.tagName, newHTML, domElt.ownerDocument);
                    if (newDomElt) {
                        this.replaceDomElement(domElt, newDomElt);
                        this.copyAttributes(domElt, newDomElt);
                        domElt = newDomElt;
                    } else {
                        // propagate the exception
                        throw ex;
                    }
                }
                // PROFILING // var msr3 = this.$startMeasure("contentchange");
                // use the delegate manager to forward a fake event
                if (aria.utils && aria.utils.Delegate) {
                    aria.utils.Delegate.delegate(aria.DomEvent.getFakeEvent('contentchange', domElt));
                }
                // PROFILING // this.$stopMeasure(msr3);
            } else {
                this.$logError(this.DIV_NOT_FOUND, [idOrElt]);
                return null;
            }
            // PROFILING // this.$stopMeasure(msr1);
            return domElt;
        },

        /**
         * Work-around to create a table element of type containerType containing the given html in IE. As IE does not
         * support setting the innerHTML property of tbody, tfoot, thead or tr elements directly. We use a simple
         * work-around: create a complete table, and get the corresponding HTML element.
         * @param {String} containerType type of the element which will be returned, and will contain the given html
         * @param {String} html html to be put inside the element of type containerType
         * @param {Object} document document to use to create the element
         * @return {HTMLElement}
         * @private
         */
        _createTableElement : function (containerType, html, document) {
            containerType = containerType.toUpperCase();
            var div = document.createElement("DIV");
            var domElt;
            if (containerType == "TBODY" || containerType == "THEAD" || containerType == "TFOOT") {
                div.innerHTML = ["<TABLE><", containerType, ">", html, "</", containerType, "></TABLE>"].join('');
                domElt = div.children[0].children[0];
            } else if (containerType == "TR") {
                div.innerHTML = ["<TABLE><TBODY><", containerType, ">", html, "</", containerType, "></TBODY></TABLE>"].join('');
                domElt = div.children[0].children[0].children[0];
            } else {
                div.innerHTML = ["<", containerType, ">", html, "</", containerType, ">"].join('');
                domElt = div.children[0];
            }
            this.$assert(194, !domElt || (domElt.tagName.toUpperCase() == containerType)); // only to be sure
            // we got the right element
            return domElt;
        },

        /**
         * Simple utility method which replaces a dom element by another.
         * @param {HTMLElement} oldElt
         * @param {HTMLElement} newElt
         */
        replaceDomElement : function (oldElt, newElt) {
            var parentNode = oldElt.parentNode;
            parentNode.insertBefore(newElt, oldElt);
            parentNode.removeChild(oldElt);
        },

        /**
         * Insert the given HTML markup into the document at the specified location. This method is cross-browser
         * compatbile. When possible, it relies on the insertAdjacentHTML method, otherwise, it relies on ranges.
         * @param {HTMLElement} domElt Reference DOM element
         * @param {String} where maybe beforeBegin, afterBegin, beforeEnd or afterEnd
         * @param {String} html HTML markup to insert at that place
         */
        insertAdjacentHTML : function (domElt, where, html) {
            if (Aria.$window.document.body.insertAdjacentHTML) {
                this.insertAdjacentHTML = function (domElt, where, html) {
                    // PROFILING // var msr = this.$startMeasure("insertAdjacentHTML");
                    // IE, Chrome, Safari, Opera
                    // simply use insertAdjacentHTML
                    try {
                        domElt.insertAdjacentHTML(where, html);
                    } catch (ex) {
                        // insertAdjacentHTML can fail in IE with tbody, tfoot, thead or tr elements
                        // We use a simple work-around: create a complete table, get the HTML element and insert it in
                        // the DOM
                        var containerElt;
                        var newDomElt;
                        if (where == "afterBegin" || where == "beforeEnd") {
                            containerElt = domElt;
                        } else if (where == "beforeBegin" || where == "afterEnd") {
                            containerElt = domElt.parentNode;
                        } else {
                            this.$logError(this.INSERT_ADJACENT_INVALID_POSITION, [where]);
                            return;
                        }
                        newDomElt = this._createTableElement(containerElt.tagName, html, domElt.ownerDocument);
                        if (newDomElt) {
                            var previousChild = newDomElt.lastChild;
                            // now let's move the html content to the right place
                            // first put lastChild at the right place, then, move the remaining children
                            if (previousChild) {
                                this.insertAdjacentElement(domElt, where, previousChild);
                                var curChild = newDomElt.lastChild;
                                while (curChild) {
                                    containerElt.insertBefore(curChild, previousChild);
                                    previousChild = curChild;
                                    curChild = newDomElt.lastChild;
                                }
                            }
                        } else {
                            // propagate the exception
                            throw ex;
                        }
                    }
                    // PROFILING // this.$stopMeasure(msr);
                };
            } else {
                this.insertAdjacentHTML = function (domElt, where, html) {
                    // PROFILING // var msr = this.$startMeasure("insertAdjacentHTML");
                    // Firefox
                    // Note that this solution could work as well with Chrome, Safari and Opera
                    var document = domElt.ownerDocument;
                    var range = document.createRange();
                    if (where == "beforeBegin" || where == "afterEnd") {
                        range.selectNode(domElt);
                    } else {
                        range.selectNodeContents(domElt);
                    }
                    var fragment = range.createContextualFragment(html);
                    this.insertAdjacentElement(domElt, where, fragment);
                    range.detach();
                    // PROFILING // this.$stopMeasure(msr);
                };
            }

            this.insertAdjacentHTML(domElt, where, html);
        },

        /**
         * Insert the given HTML element into the document at the specified location. This method is cross-browser
         * compatbile. When possible, it relies on the insertAdjacentElement method, otherwise, it relies on
         * insertBefore and appendChild.
         * @param {HTMLElement} domElt Reference DOM element
         * @param {String} where maybe beforeBegin, afterBegin, beforeEnd or afterEnd
         * @param {String} html HTML markup to insert at that place
         */
        insertAdjacentElement : function (domElt, where, newElement) {
            if (Aria.$window.document.body.insertAdjacentElement) {
                this.insertAdjacentElement = function (domElt, where, newElement) {
                    domElt.insertAdjacentElement(where, newElement);
                };
            } else {
                // Firefox :'(
                this.insertAdjacentElement = function (domElt, where, newElement) {
                    if (where == "beforeBegin") {
                        domElt.parentNode.insertBefore(newElement, domElt);
                    } else if (where == "afterBegin") {
                        domElt.insertBefore(newElement, domElt.firstChild);
                    } else if (where == "beforeEnd") {
                        domElt.appendChild(newElement);
                    } else if (where == "afterEnd") {
                        domElt.parentNode.insertBefore(newElement, domElt.nextSibling);
                    } else {
                        this.$logError(this.INSERT_ADJACENT_INVALID_POSITION, [where]);
                    }
                };
            }

            this.insertAdjacentElement(domElt, where, newElement);
        },

        /**
         * Copy attributes from one dom element to another.
         * @param {HTMLElement} src Source element
         * @param {HTMLElement} dest Destination element
         */
        copyAttributes : function (src, dest) {
            // IE has a mergeAttributes method which does exactly that. Let's use it directly.
            // Note that copying attributes with a loop on attributes and setAttributes has strange results on IE7 (for
            // example, a TR can appear as disabled)
            if (Aria.$window.document.body.mergeAttributes) {
                this.copyAttributes = function (src, dest) {
                    dest.mergeAttributes(src, false);
                };
            } else {
                this.copyAttributes = function (src, dest) {
                    // on other browsers, let's copy the attributes manually:
                    var srcAttr = src.attributes;
                    for (var i = 0, l = srcAttr.length; i < l; i++) {
                        var attr = srcAttr[i];
                        dest.setAttribute(attr.name, attr.value);
                    }
                };
            }

            this.copyAttributes(src, dest);
        },

        /**
         * Gets the dimensions of the viewport. Extracted from Closure's documentation.
         *
         * <pre>
         * *
         * Gecko standards mode:
         * docEl.clientWidth  Width of viewport excluding scrollbar.
         * win.innerWidth     Width of viewport including scrollbar.
         * body.clientWidth   Width of body element.
         *
         * docEl.clientHeight Height of viewport excluding scrollbar.
         * win.innerHeight    Height of viewport including scrollbar.
         * body.clientHeight  Height of document.
         *
         * Gecko Backwards compatible mode:
         * docEl.clientWidth  Width of viewport excluding scrollbar.
         * win.innerWidth     Width of viewport including scrollbar.
         * body.clientWidth   Width of viewport excluding scrollbar.
         *
         * docEl.clientHeight Height of document.
         * win.innerHeight    Height of viewport including scrollbar.
         * body.clientHeight  Height of viewport excluding scrollbar.
         *
         * IE6/7 Standards mode:
         * docEl.clientWidth  Width of viewport excluding scrollbar.
         * win.innerWidth     Undefined.
         * body.clientWidth   Width of body element.
         *
         * docEl.clientHeight Height of viewport excluding scrollbar.
         * win.innerHeight    Undefined.
         * body.clientHeight  Height of document element.
         *
         * IE5 + IE6/7 Backwards compatible mode:
         * docEl.clientWidth  0.
         * win.innerWidth     Undefined.
         * body.clientWidth   Width of viewport excluding scrollbar.
         *
         * docEl.clientHeight 0.
         * win.innerHeight    Undefined.
         * body.clientHeight  Height of viewport excluding scrollbar.
         *
         * Opera 9 Standards and backwards compatible mode:
         * docEl.clientWidth  Width of viewport excluding scrollbar.
         * win.innerWidth     Width of viewport including scrollbar.
         * body.clientWidth   Width of viewport excluding scrollbar.
         *
         * docEl.clientHeight Height of document.
         * win.innerHeight    Height of viewport including scrollbar.
         * body.clientHeight  Height of viewport excluding scrollbar.
         *
         * WebKit:
         * Safari 2
         * docEl.clientHeight Same as scrollHeight.
         * docEl.clientWidth  Same as innerWidth.
         * win.innerWidth     Width of viewport excluding scrollbar.
         * win.innerHeight    Height of the viewport including scrollbar.
         * frame.innerHeight  Height of the viewport exluding scrollbar.
         *
         * Safari 3 (tested in 522)
         *
         * docEl.clientWidth  Width of viewport excluding scrollbar.
         * docEl.clientHeight Height of viewport excluding scrollbar in strict mode.
         * body.clientHeight  Height of viewport excluding scrollbar in quirks mode.
         * </pre>
         *
         * @return {aria.utils.DomBeans:Size} Size object width 'width' and 'height' properties
         * @private
         */
        _getViewportSize : function () {
            if (ariaCoreBrowser.isIOS || ariaCoreBrowser.isAndroid) {
                // Initially (without user-initiated zoom) window's dimensions are the same or nearly the same as
                // documentElement's.
                // Note however that documentElement's clientWidth/Height is unaffected by user zoom while window's
                // innerWidth/Height change on zoom.
                // Also, in Safari on ipad, the viewport resizes when scrolling the page (the address bar is shrinked
                // gradually) and documentElement's size is unaffected, while window's size changes (but in Chrome on
                // mobile, both change).

                // See also http://jakub-g.github.io/quirksmode/widthtest.html
                return {
                    'width' : Aria.$window.innerWidth,
                    'height' : Aria.$window.innerHeight
                };
            } else {
                var docEl = Aria.$window.document.documentElement;
                return {
                    'width' : docEl.clientWidth,
                    'height' : docEl.clientHeight
                };
            }
        },

        /**
         * @return {aria.utils.DomBeans:Size} Size object width 'width' and 'height' properties.<br>
         * This method returns the size in pixels of the <strong>visible on screen portion of the viewport</strong> (in
         * contrast to the so called "virtual viewport" on the mobile devices). It is suitable for usages like
         * determining the size of the overlays covering whole or part of the page.<br>
         * <br>
         * Note that viewport size may change in many situations, such as when the user:<br> - resizes the browser,<br> -
         * rotates the mobile device,<br> - scrolls the page (on mobile devices),<br> - zooms the page (on mobile
         * devices),<br>
         * as well as on desktop browsers when some elements are added to/removed from the DOM (since viewport size
         * excludes scrollbars, which may or may not appear on the page in desktop browsers).<br>
         * <br>
         * If you depend on the precise viewport size information, you should listen to <tt>window.onresize</tt> and
         * <tt>document.onscroll</tt> events.<br>
         * <br>
         * See also http://jakub-g.github.io/quirksmode/widthtest.html
         */
        getViewportSize : function () {
            return this._getViewportSize();
        },

        /**
         * Gets the document scroll distance as a coordinate object.
         * @parameter {Object} base If provided, the total scrolling offset of the base element is calculated, minus the
         * scrolling offset of the document element
         * @return {Object} Object with values 'scrollLeft' and 'scrollTop'.
         * @protected
         */
        _getDocumentScroll : function (base) {
            var document = base && base.ownerDocument ? base.ownerDocument : Aria.$window.document;
            var scrollLeft = 0;
            var scrollTop = 0;
            var documentScroll = this.getDocumentScrollElement(document);

            if (base != null) {
                var next = base;
                while (next != null) {
                    if (next.scrollLeft) {
                        scrollLeft += next.scrollLeft;
                    }
                    if (next.scrollTop) {
                        scrollTop += next.scrollTop;
                    }
                    next = next.parentNode;
                }
                // now subtract scroll offset of the document
                scrollLeft -= documentScroll.scrollLeft;
                scrollTop -= documentScroll.scrollTop;

            } else {
                scrollLeft = documentScroll.scrollLeft;
                scrollTop = documentScroll.scrollTop;
            }

            // this will be used to insert elements in the body, so body scrolls needs to be taken in consideration also
            if (documentScroll != document.body) {
                scrollLeft += document.body.scrollLeft;
                scrollTop += document.body.scrollTop;
            }

            var scroll = {
                'scrollLeft' : scrollLeft,
                'scrollTop' : scrollTop
            };
            return scroll;
        },

        /**
         * Get the full size of the page. It is bigger than the viewport size if there are scrollbars, otherwise it's
         * the viewport size
         * @return {aria.utils.DomBeans:Size} Size object
         */
        getFullPageSize : function () {
            // https://developer.mozilla.org/en/DOM/window.scrollMaxY suggests not to use scrollMaxY
            // Take the maximum between the document and the viewport srollWidth/Height
            // PTR 04677501 AT-Release1.0-36 release bug fixing: Safari has the correct scrollWidth and scrollHeight in
            // document.body
            var doc = this.getDocumentScrollElement();

            var docSize = {
                width : doc.scrollWidth,
                height : doc.scrollHeight
            };

            var vieportSize = this._getViewportSize();

            return {
                width : Math.max(docSize.width, vieportSize.width),
                height : Math.max(docSize.height, vieportSize.height)
            };
        },

        /**
         * Get the position in a page of an element. It is relative to the current viewport (i.e. depends on scroll of
         * the page)
         * @param {HTMLElement} element
         * @param {Boolean} stopAbsolute <i>[optional, default=false]</i> if true, it will stop calculating the
         * position when it encounters the first ancestor of the element with absolute positioning
         * @return {aria.utils.DomBeans:Position}
         */
        calculatePosition : function (element, stopAbsolute) {
            if (!element) {
                return;
            }

            var document = element.ownerDocument;
            // shortcut to Browser
            var browser = ariaCoreBrowser;

            var offsetLeft = 0;
            var offsetTop = 0;
            var scrollTop = 0;
            var scrollLeft = 0;

            var scrollNotIncluded = true;

            // Get offset information
            var i = 0;
            /* Get the dom in case of JS object */
            var obj = element, objPositionCss;

            if (obj.getBoundingClientRect && !stopAbsolute) {
                // IE getBoundingClientRect have extra pixels.
                // cf https://github.com/jquery/jquery/blob/master/src/offset.js#L101-106
                var shiftLeft = browser.isIE7 ? document.documentElement.clientLeft : 0;
                var shiftTop = browser.isIE7 ? document.documentElement.clientTop : 0;
                // IE throws an error if the element is not in DOM
                try {
                    var rect = obj.getBoundingClientRect();
                    scrollNotIncluded = false;
                    offsetLeft = rect.left - shiftLeft;
                    offsetTop = rect.top - shiftTop;

                } catch (er) {
                    offsetLeft = 0;
                    offsetTop = 0;
                }
            } else {
                while (obj && obj.parentNode) {
                    objPositionCss = aria.utils.Dom.getStyle(obj, "position");

                    if (stopAbsolute && i > 0 && objPositionCss == "absolute") {
                        break;
                    }
                    offsetLeft += obj.offsetLeft;
                    offsetTop += obj.offsetTop;

                    obj = obj.offsetParent;
                    i++;
                }
            }

            // Get scroll information
            i = 0;
            obj = element;

            while (obj && obj.parentNode && obj.parentNode != document.body && obj.parentNode.tagName) {

                if (!browser.isOpera && obj.nodeName != 'HTML') {
                    if (obj.scrollTop !== 0 && obj.scrollTop) {
                        scrollTop = obj.scrollTop;
                        if (scrollNotIncluded) {
                            offsetTop -= obj.scrollTop;
                        }
                    } else if (obj.scrollLeft !== 0 && obj.scrollLeft) {
                        scrollLeft = obj.scrollLeft;
                        if (scrollNotIncluded) {
                            offsetLeft -= obj.scrollLeft;
                        }
                    }
                } else if (browser.isOpera) {
                    if (obj.scrollTop !== 0 && obj.scrollTop != obj.offsetTop) {
                        if (scrollNotIncluded) {
                            offsetTop -= obj.scrollTop;
                        }
                        scrollTop = obj.scrollTop;
                    } else if (obj.scrollLeft !== 0 && obj.scrollLeft != obj.offsetLeft) {
                        scrollLeft = obj.scrollLeft;
                        if (scrollNotIncluded) {
                            offsetLeft -= obj.scrollLeft;
                        }
                    }
                }

                // scrollOffset calculation did not take care of the stopAbsolute
                objPositionCss = aria.utils.Dom.getStyle(obj, "position");
                if (i > 0 && objPositionCss == "absolute") {
                    break;
                }

                i++;
                obj = obj.parentNode;
            }

            var position = {
                top : Math.round(offsetTop),
                left : Math.round(offsetLeft),
                scrollTop : scrollTop,
                scrollLeft : scrollLeft
            };

            return position;
        },

        /**
         * Get the client geometry of an element. It means the coordinates of the top/left corner and the width and
         * height of the area inside the element which can be used by its child elements. Returned value is relative to
         * the current viewport.
         * @param {HTMLElement} element Element whose client geometry is requested.
         * @return {aria.utils.DomBeans:Geometry} Geometry object
         */
        getClientGeometry : function (element) {
            var position = this.calculatePosition(element);
            return {
                x : position.left + element.clientLeft,
                y : position.top + element.clientTop,
                width : element.clientWidth,
                height : element.clientHeight
            };
        },

        /**
         * Get the offset top and left of an element (relative to its offset parent), based either on the style or the
         * offset element. Useful to manage some internet explorer offset issues.
         * @param {HTMLElement} element Element whose offset is requested.
         * @return {Object} JSON with the top ad left properties.
         * @public
         */
        getOffset : function (element) {
            var positionStyle = this.getStyle(element, "position");
            var isAbsolute = positionStyle === "absolute";
            var isFixed = positionStyle === "fixed";
            var position = {};
            if (isAbsolute) {
                position.left = this.getStylePx(element, "left", null);
                position.top = this.getStylePx(element, "top", null);
            }
            if (position.left == null || position.top == null) {
                var offsetParent = isFixed ? Aria.$window.document.body : element.offsetParent;
                var offsetParentPosition = this.calculatePosition(offsetParent);
                var elementPosition = this.calculatePosition(element);
                if (position.left == null) {
                    var borderLeft = this.getStylePx(offsetParent, "borderLeftWidth", 0);
                    position.left = elementPosition.left - offsetParentPosition.left + offsetParent.scrollLeft
                            - borderLeft;
                }
                if (position.top == null) {
                    var borderTop = this.getStylePx(offsetParent, "borderTopWidth", 0);
                    position.top = elementPosition.top - offsetParentPosition.top + offsetParent.scrollTop - borderTop;
                }
            }

            return position;
        },

        /**
         * Get the geometry of an element, relative to the current viewport. It means the coordinates of top/left corner
         * and its width and height. If the element is the body, the geometry corresponds to the whole page, otherwise
         * it's the visible part of the element.
         * @param {HTMLElement} element Element whose geometry is requested.
         * @return {aria.utils.DomBeans:Geometry} Geometry object
         * @public
         */
        getGeometry : function (element) {
            if (!element) {
                return null;
            }
            var document = element.ownerDocument;

            if (element === document.body) {
                var size = this.getFullPageSize();

                return {
                    x : 0,
                    y : 0,
                    width : size.width,
                    height : size.height
                };

            } else {
                var width, height;
                var browser = ariaCoreBrowser;
                if (browser.isChrome || browser.isSafari) {
                    var rectTextObject = element.getBoundingClientRect();
                    width = Math.round(rectTextObject.width);
                    height = Math.round(rectTextObject.height);
                } else {
                    width = element.offsetWidth;
                    height = element.offsetHeight;
                }

                var position = this.calculatePosition(element);
                var res = {
                    x : position.left,
                    y : position.top,
                    width : width,
                    height : height
                };

                // PTR 04606169:
                // adapt res if one of the parent overflow property prevents parts of the item from being visible
                var parent = element;
                // we do not need to go up to html or body element (and it can lead to wrong results
                // especially in IE7)
                var documentElement = document.documentElement;
                var body = document.body;
                do {
                    var parentPosition = this.getStyle(parent, "position");
                    if (parentPosition == "absolute" || parentPosition == "fixed") {
                        parent = parent.offsetParent;
                    } else {
                        parent = parent.parentNode;
                    }
                    if (!parent || parent == documentElement || parent == body) {
                        return res;
                    }

                    // taking client geometry of the parent so that it does not include scrollbars
                    var parentGeometry = this.getClientGeometry(parent);
                    var parentOverflowX = this.getStyle(parent, "overflowX");
                    var parentOverflowY = this.getStyle(parent, "overflowY");

                    var delta;
                    if (parentOverflowX != "visible") {
                        delta = parentGeometry.x - res.x;
                        if (delta > 0) {
                            res.x += delta;
                            res.width -= delta;
                            if (res.width < 0) {
                                return null; // item is not visible
                            }
                        }
                        delta = res.x + res.width - parentGeometry.x - parentGeometry.width;
                        if (delta > 0) {
                            res.width -= delta;
                            if (res.width < 0) {
                                return null; // item is not visible
                            }
                        }
                    }
                    if (parentOverflowY != "visible") {
                        delta = parentGeometry.y - res.y;
                        if (delta > 0) {
                            res.y += delta;
                            res.height -= delta;
                            if (res.height < 0) {
                                return null; // item is not visible
                            }
                        }
                        delta = res.y + res.height - parentGeometry.y - parentGeometry.height;
                        if (delta > 0) {
                            res.height -= delta;
                            if (res.height < 0) {
                                return null; // item is not visible
                            }
                        }
                    }
                } while (true);
            }
        },

        /**
         * Retrieve the computed style for a given CSS property on a given DOM element.
         * @param {HTMLElement} element The DOM element on which to retrieve a CSS property
         * @param {String} property The CSS property to retrieve. For maximum portability, it should be in camel case
         * (for instance <code>backgroundImage</code>)
         */
        getStyle : function (element, property) {
            var browser = ariaCoreBrowser;
            var isIE8orLess = browser.isIE8 || browser.isIE7;
            if (isIE8orLess) {
                this.getStyle = function (element, property) {
                    if (property == 'opacity') {// IE<=8 opacity uses filter
                        var val = 100;
                        try { // will error if no DXImageTransform
                            val = element.filters['DXImageTransform.Microsoft.Alpha'].opacity;
                        } catch (e) {
                            try { // make sure its in the document
                                val = element.filters('alpha').opacity;
                            } catch (er) {}
                        }
                        return (val / 100).toString(10); // to be consistent with getComputedStyle
                    } else if (property == 'width' || property == 'height') {
                        return ariaUtilsCssUnits.getDomWidthOrHeightForOldIE(element, property);
                    } else if (property == 'float') { // fix reserved word
                        property = 'styleFloat'; // fall through
                    }
                    var value;
                    // test currentStyle before touching
                    if (element.currentStyle) {
                        value = element.currentStyle[property];
                        if (!value) {
                            // Try the camel case
                            var camel = ariaUtilsString.dashedToCamel(property);
                            value = element.currentStyle[camel];
                        }
                    }
                    return (value || element.style[property]);
                };
            } else {
                this.getStyle = function (element, property) {
                    var window = Aria.$window;
                    var value = null;
                    if (property == "float") {
                        property = "cssFloat";
                    } else if (property == "backgroundPositionX" || property == "backgroundPositionY") {
                        // backgroundPositionX and backgroundPositionY are not standard
                        var backgroundPosition = this.getStyle(element, "backgroundPosition");
                        if (backgroundPosition) {
                            var match = /^([-.0-9a-z%]+)\s([-.0-9a-z%]+)($|,)/.exec(backgroundPosition);
                            if (match) {
                                value = ((property == "backgroundPositionX") ? match[1] : match[2]);
                            }
                        }
                        return value;
                    }
                    var computed = window.getComputedStyle(element, "");
                    if (computed) {
                        value = computed[property];
                    }
                    return (value || element.style[property]);
                };
            }

            return this.getStyle(element, property);
        },

        /**
         * Center the given size in the viewport.
         * @param {aria.utils.DomBeans:Size} size size of the element to center in the viewport
         * @param {Object} base The base element used to account for scrolling offsets
         * @return {aria.utils.DomBeans:Position} position of the element when centered in the viewport
         */
        centerInViewport : function (size, base) {
            var viewportSize = this._getViewportSize();
            var documentScroll = this._getDocumentScroll(base);
            return {
                left : parseInt(documentScroll.scrollLeft + (viewportSize.width - size.width) / 2, 10),
                top : parseInt(documentScroll.scrollTop + (viewportSize.height - size.height) / 2, 10)
            };
        },

        /**
         * Check if a given position + size couple can fit in the current viewport
         * @param {aria.utils.DomBeans:Position} position
         * @param {aria.utils.DomBeans:Size} size
         * @param {Object} base The base element used to account for scrolling offsets
         * @return {Boolean} True if the given position+size couple can fit in the current viewport
         */
        isInViewport : function (position, size, base) {
            var viewportSize = this._getViewportSize();
            var documentScroll = this._getDocumentScroll(base);

            if (position.top < documentScroll.scrollTop || position.left < documentScroll.scrollLeft
                    || position.top + size.height > documentScroll.scrollTop + viewportSize.height
                    || position.left + size.width > documentScroll.scrollLeft + viewportSize.width) {
                return false;
            } else {
                return true;
            }
        },

        /**
         * Compares two geometries to ascertain whether an element is inside another one
         * @param {aria.utils.DomBeans:Geometry} needle
         * @param {aria.utils.DomBeans:Geometry|aria.utils.Dom:VIEWPORT:property} haystack
         * @return {Boolean}
         */
        isInside : function (needle, haystack) {
            needle.width = needle.width || 0;
            needle.height = needle.height || 0;
            if (haystack == this.VIEWPORT) {
                return this.isInViewport({
                    left : needle.x,
                    top : needle.y
                }, {
                    width : needle.width,
                    height : needle.height
                });
            }
            haystack.width = haystack.width || 0;
            haystack.height = haystack.height || 0;
            if (needle.x < haystack.x || needle.y < haystack.y || needle.x + needle.width > haystack.x + haystack.width
                    || needle.y + needle.height > haystack.y + haystack.height) {
                return false;
            }
            return true;
        },

        /**
         * Given a position + size couple, return a corrected position that should fit in the viewport. If the size is
         * bigger than the vieport it returns a position such that the top left corner of the element to be fit is in
         * the viewport.
         * @param {aria.utils.DomBeans:Position} position
         * @param {aria.utils.DomBeans:Size} size
         * @param {Object} base The base element used to account for scrolling offsets
         * @return {aria.utils.DomBeans:Position}
         */
        fitInViewport : function (position, size, base) {
            var viewportSize = this._getViewportSize();
            var documentScroll = this._getDocumentScroll(base);

            var minTopValue = documentScroll.scrollTop;
            var maxTopValue = Math.max(0, documentScroll.scrollTop + viewportSize.height - size.height);
            var top = aria.utils.Math.normalize(position.top, minTopValue, maxTopValue);

            var minLeftValue = documentScroll.scrollLeft;
            var maxLeftValue = Math.max(0, documentScroll.scrollLeft + viewportSize.width - size.width);
            var left = aria.utils.Math.normalize(position.left, minLeftValue, maxLeftValue);

            return {
                'top' : top,
                'left' : left
            };
        },

        /**
         * Fits a geometry into another geometry
         * @param {aria.utils.DomBeans:Geometry} geometry
         * @param {aria.utils.DomBeans:Geometry|aria.utils.Dom:VIEWPORT:property} container
         * @return {aria.utils.DomBeans:Position} top and left values of the fitted geometry
         */
        fitInside : function (geometry, container) {
            geometry.width = geometry.width || 0;
            geometry.height = geometry.height || 0;
            if (container == this.VIEWPORT) {
                container = this.getViewportSize();
                container.x = container.y = 0;
            }
            container.width = container.width || 0;
            container.height = container.height || 0;
            var left = (geometry.x < container.x) ? container.x : geometry.x;
            var top = (geometry.y < container.y) ? container.y : geometry.y;
            if (geometry.x + geometry.width > container.x + container.width) {
                left = container.x + container.width - geometry.width;
            }
            if (geometry.y + geometry.height > container.y + container.height) {
                top = container.y + container.height - geometry.height;
            }
            return {
                'top' : top,
                'left' : left
            };

        },

        /**
         * Check whether a child element is a child element of a particular parentNode
         * @param {HTMLElement} child The child DOM element to check
         * @param {HTMLElement} parent The parent DOM element to check
         * @return {Boolean} True if the given child is a child of the given parent
         */
        isAncestor : function (child, parent) {
            if (!(child && child.ownerDocument)) {
                return false;
            }
            var document = child.ownerDocument;
            var body = document.body;
            var element = child;
            while (element && element != body) {
                if (element == parent) {
                    return true;
                }
                element = element.parentNode;
            }
            return (element == parent); // can be true if parent == body and element is present in DOM
        },

        /**
         * Check whether an HTML element is in the DOM (a descendant of document.body).
         * @param {HTMLElement} element The HTML element to check
         * @return {Boolean} true if the HTML element is in the DOM hierarchy
         */
        isInDom : function (element) {
            return element ? this.isAncestor(element, element.ownerDocument.body) : false;
        },

        /**
         * Removes an element from the DOM
         * @param {HTMLElement} element
         */
        removeElement : function (element) {
            if (element.parentNode) {
                element.parentNode.removeChild(element);
            }
        },

        /**
         * Change scrollbar positions to ensure that an item is visible.
         * @param {HTMLElement} element element which is to be made visible
         * @param {Boolean} alignTop if true align top of the element with top of container. if false, align bottom. If
         * not specified, does "minimal job".
         */
        scrollIntoView : function (element, alignTop) {
            var document = element.ownerDocument;
            var origin = element, originRect = origin.getBoundingClientRect();
            var hasScroll = false;
            var documentScroll = this.getDocumentScrollElement(document);

            while (element) {

                if (element == document.body) {
                    element = documentScroll;
                } else {
                    element = element.parentNode;
                }

                if (element) {
                    var hasScrollbar = (!element.clientHeight) ? false : element.scrollHeight > element.clientHeight;

                    if (!hasScrollbar && element == documentScroll && documentScroll == document.body) {
                        var docElement = document.documentElement;
                        // On Chrome, documentScroll == document.body due to https://code.google.com/p/chromium/issues/detail?id=157855
                        // But if the body has no margin: document.body.scrollHeight == document.body.clientHeight
                        // and hasScrollbar can be false even if there is a scrollbar. That's why the following line was added:
                        hasScrollbar = (!docElement || !docElement.clientHeight) ? false : docElement.scrollHeight > docElement.clientHeight;
                    }

                    if (!hasScrollbar) {
                        if (element == documentScroll) {
                            element = null;
                        }
                        continue;
                    }

                    var rects;
                    if (element == documentScroll) {
                        rects = {
                            left : 0,
                            top : 0
                        };
                    } else {
                        rects = element.getBoundingClientRect();
                    }

                    // check that elementRect is in rects
                    var deltaLeft = originRect.left - (rects.left + (parseInt(element.style.borderLeftWidth, 10) | 0));
                    var deltaRight = originRect.right
                            - (rects.left + element.clientWidth + (parseInt(element.style.borderLeftWidth, 10) | 0));
                    var deltaTop = originRect.top - (rects.top + (parseInt(element.style.borderTopWidth, 10) | 0));
                    var deltaBottom = originRect.bottom
                            - (rects.top + element.clientHeight + (parseInt(element.style.borderTopWidth, 10) | 0));

                    // adjust display depending on deltas
                    if (deltaLeft < 0) {
                        element.scrollLeft += deltaLeft;
                    } else if (deltaRight > 0) {
                        element.scrollLeft += deltaRight;
                    }

                    if (alignTop === true && !hasScroll) {
                        element.scrollTop += deltaTop;
                    } else if (alignTop === false && !hasScroll) {
                        element.scrollTop += deltaBottom;
                    } else {
                        if (deltaTop < 0) {
                            element.scrollTop += deltaTop;
                        } else if (deltaBottom > 0) {
                            element.scrollTop += deltaBottom;
                        }
                    }

                    if (element == documentScroll) {
                        element = null;
                    } else {
                        // readjust element position after scrolls, and check if vertical scroll has changed.
                        // this is required to perform only one alignment
                        var nextRect = origin.getBoundingClientRect();
                        if (nextRect.top != originRect.top) {
                            hasScroll = true;
                        }
                        originRect = nextRect;
                    }
                }
            }
        },

        /**
         * Get the the document scroll element (hidden element used to measure things).
         * @param {Object} document document to use (optional, defaults to Aria.$window.document)
         * @return {HTMLElement} documentElement
         */
        getDocumentScrollElement : function (document) {
            if (document == null) {
                document = Aria.$window.document;
            }
            return ((!(ariaCoreBrowser.isSafari || ariaCoreBrowser.isChrome || ariaCoreBrowser.isEdge) && (document.compatMode == "CSS1Compat"))
                    ? document.documentElement
                    : document.body);
        },

        /**
         * Set the opacity of an element
         * @param {HTMLElement} element
         * @param {Number} opacity must be between 0 and 1
         */
        setOpacity : function (element, opacity) {
            var browser = ariaCoreBrowser;
            var isIE8OrLess = (browser.isIE8 || browser.isIE7);
            this.setOpacity = isIE8OrLess ? this._setOpacityLegacyIE : this._setOpacityW3C;
            this.setOpacity(element, opacity);
        },

        /**
         * Set the opacity of an element on IE<=8
         * @private
         * @param {HTMLElement} element
         * @param {Number} opacity must be between 0 and 1
         */
        _setOpacityLegacyIE : function (element, opacity) {
            element.style.cssText += ";filter:alpha(opacity=" + (opacity * 100) + ");";
        },

        /**
         * Set the opacity of an element on modern browsers (IE9+ and non-IE)
         * @private
         * @param {HTMLElement} element
         * @param {Number} opacity must be between 0 and 1
         */
        _setOpacityW3C : function (element, opacity) {
            element.style.opacity = opacity;
        },

        /**
         * Refreshes scrollbars for the specified DOM element and all its parents.
         * @see refreshScrollbars
         * @param {HTMLElement} domElt
         */
        _refreshScrollbarsFix : function (domElt) {
            // work-around for http://crbug.com/240772
            var values = [];
            var parent = domElt;
            while (parent && parent.style) {
                values.push(parent.style.cssText);
                parent.style.overflow = "hidden";
                parent = parent.parentNode;
            }
            domElt.getBoundingClientRect(); // making sure Chrome updates the display
            parent = domElt;
            while (parent && parent.style) {
                parent.style.cssText = values.shift();
                parent = parent.parentNode;
            }
        },

        /**
         * Checks whether <a href="http://crbug.com/240772">this Chrome scrollbars bug</a> (also appearing on other
         * webkit-based browsers) is still there.
         * @see refreshScrollbars
         * @return {Boolean} true if Webkit is used and the bug is still there. false otherwise.
         */
        _checkRefreshScrollbarsNeeded : function () {
            if (!ariaCoreBrowser.isWebkit) {
                // we only do this check on Webkit
                return false;
            }
            var document = Aria.$window.document;
            var testElt = document.createElement("div");
            testElt.style.cssText = "overflow:auto;width:100px;height:100px;left:-1000px;top:-1000px;";
            testElt.innerHTML = '<div style="width:150px;height:100px;"></div>';
            document.body.appendChild(testElt);
            // firstCheck below is always true, computing it is necessary so that Chrome knows a scrollbar is
            // needed at this stage
            var firstCheck = testElt.scrollWidth > testElt.clientWidth;
            testElt.firstChild.style.width = "100px";
            var secondCheck = testElt.scrollWidth > testElt.clientWidth; // should be false, but is true on Chrome
            document.body.removeChild(testElt);
            return firstCheck && secondCheck;
        },

        /**
         * Refreshes scrollbars for the specified DOM element and all its parents. It is needed for some webkit-based
         * browsers as a work-around for <a href="http://crbug.com/240772">this bug</a>, otherwise scrollbars are not
         * removed when they are no longer necessary if the content has nearly the same size as the container.<br>
         * This method does nothing on other browsers. On Webkit, the first time it is called, it checks whether the
         * work-around is still needed and does nothing if the bug was fixed.
         * @param {HTMLElement} element
         */
        refreshScrollbars : function (domElt) {
            this.refreshScrollbars = this._checkRefreshScrollbarsNeeded() ? this._refreshScrollbarsFix : Aria.empty;
            this.refreshScrollbars(domElt);
        },

        /**
         * Calls getStyle(domElt, property) and returns the value as a number if it is in pixels. Otherwise, returns the
         * provided default value.
         * @param {HTMLElement} element The DOM element on which to retrieve a CSS property
         * @param {String} property The CSS property to retrieve
         * @param {Number} defaultValue default value to return in case getStyle does not return a value in pixels
         * @return {Number}
         */
        getStylePx : function (element, property, defaultValue) {
            var value = this.getStyle(element, property);
            if (this.pxRegExp.test(value)) {
                return Math.round(parseFloat(value));
            }
            return defaultValue;
        },

        /**
         * Proxy/polly fill method for getElementsByClassName. On browser which don't support this feature the behaviour
         * is emulated, otherwise the native version is called.
         * @param {HTMLElement} dom The source dom element
         * @param {String} className The class name to look for
         * @return {Array} Array of Html elements
         */
        getElementsByClassName : function (domElement, className) {
            if (!domElement) {
                return [];
            }
            className = String(className); // Fx 3.6 compat
            if (domElement.getElementsByClassName) {
                return domElement.getElementsByClassName(className);
            } else {
                var elements = domElement.getElementsByTagName("*");
                var found = [];
                var regexp = new RegExp("\\b" + className + "\\b");
                for (var i = 0, ii = elements.length; i < ii; i++) {
                    var el = elements[i];
                    if (regexp.test(el.className)) {
                        found.push(el);
                    }
                }
                return found;
            }
        }
    }
});
