/**
 * @class ST.Element
 * This class wraps a DOM element and provides helpful methods that simplify and normalize
 * browser differences.
 */
ST.Element = ST.define(function (Element) {
    var doc = document,
        docEl = doc.documentElement,
        body = doc.body,
        propertyCache = {},
        spaceRe = /[ ]+/g,
        camelRe = /(-[a-z])/gi,
        camelReplaceFn = function(m, a) {
            return a.charAt(1).toUpperCase();
        },
        inputTags = {
            INPUT: 1,
            TEXTAREA: 1
        },
        // Input types that cannot be typed into 
        nonEditableInputTypes = {
            button: 1,
            checkbox: 1,
            hidden: 1,
            image: 1,
            radio: 1,
            reset: 1,
            submit: 1
        };
 
    /**
     * Normalizes CSS property keys from dash delimited to camel case JavaScript Syntax.
     * For example:
     *
     * - border-width -> borderWidth
     * - padding-top -> paddingTop
     *
     * @method normalize
     * @static
     * @private
     * @param {String} prop The property to normalize
     * @return {String} The normalized string
     */
    function normalize(prop) {
        return propertyCache[prop] || (propertyCache[prop] = prop.replace(camelRe, camelReplaceFn));
    }
 
    return {
        isElement: true,
 
        constructor: function(element) {
            this.dom = element;
        },
 
        contains: function (el) {
            var dom = this.dom,
                child = el.dom || el;
 
            if (dom.contains) {
                return dom.contains(child);
            }
 
            for (; child; child = child.parentNode) {
                if (child === dom) {
                    return true;
                }
            }
 
            return false;
        },
 
        /**
         * Try to focus the element.
         *
         * @return {ST.Element} this
         */
        focus: function() {
            var dom = this.dom;
 
            if (dom) {
                dom.focus();
            }
 
            return this;
        },
 
        getBox: function () {
            var round = Math.round,
                dom = this.dom,
                box, scroll, x, y, w, h;
 
            if (dom !== doc && dom !== body) {
                // IE (including IE10) throws an error when getBoundingClientRect 
                // is called on an element not attached to dom 
                try {
                    box = dom.getBoundingClientRect();
                } catch (ex) {
                    box = { left: 0, top: 0, width: 0, height: 0 };
                }
 
                x = round(box.left);
                y = round(box.top);
                w = box.width;
                h = box.height;
 
                scroll = new ST.Element(doc).getScroll();
 
                x += scroll.x;
                y += scroll.y;
            } else {
                x = y = w = h = 0;
            }
 
            return { x: x, y: y, w: w, h: h };
        },
 
        getXY: function () {
            var box = this.getBox();
 
            return [ box.x, box.y ];
        },
 
        getStyle: function (prop) {
            var dom = this.dom,
                view = dom.ownerDocument.defaultView,
                style;
 
            if (view && view.getComputedStyle) {
                style = view.getComputedStyle(dom, null);
            } else {
                style = dom.currentStyle;
            }
 
            return (style || dom.style)[normalize(prop)];
        },
 
        getScroll: function() {
            var me = this,
                scroller = me.getScroller(),
                dom = me.dom,
                pos;
 
            if (scroller) {
                pos = scroller.getPosition();
            } else {
                if (dom === doc || dom === docEl || dom == body) {
                    dom = me.self.getViewportScrollElement();
                }
 
                pos = {
                    x: dom ? dom.scrollLeft : 0,
                    y: dom ? dom.scrollTop : 0
                };
            }
 
            return pos;
        },
 
        scrollTo: function(x, y) {
            var me = this,
                scroller = me.getScroller(),
                dom = me.dom;
 
            if (scroller) {
                scroller.scrollTo(x, y);
            } else {
                if (dom === doc || dom === docEl || dom == body) {
                    dom = me.self.getViewportScrollElement();
                }
 
                if (dom) {
                    dom.scrollLeft = x;
                    dom.scrollTop = y;
                }
            }
        },
 
        getClassMap: function () {
            var me = this,
                className = me.dom.className,
                classMap = me._classMap;
 
            if (!classMap || me._className !== className) {
                me._className = className = (className || '');
                me._classMap = classMap = ST.Array.toMap(className.split(spaceRe));
                delete classMap[''];
            }
 
            return classMap;
        },
 
        /**
         * Returns the `Ext.Component` associated with this element.
         *
         * @return {Ext.Component}
         */
        getComponent: function() {
            var Ext = window.Ext,
                cmp = null,
                dom = this.dom,
                Comp, Mgr;
 
            if (Ext) {
                Comp = Ext.Component;
                if (Comp) {
                    if (Comp.fromElement && !Comp.fromElement.$emptyFn) {
                        // 5.1.1-5.1.x, 6.0.1+ (6.0.0 has an emptyFn placeholder) 
                        cmp = Comp.fromElement(dom);
                    } else if (Comp.getComponentByElement) {
                        // 5.0.0, 5.0.1 
                        try {
                            cmp = Comp.getComponentByElement(dom);
                        } catch (e) {}
                    } else {
                        Mgr = Ext.ComponentManager;
 
                        if (Mgr) {
                            if (Mgr.byElement) {
                                //5.1.0 
                                cmp = Mgr.byElement(dom);
                            } else {
                                //Ext 4.x, Sencha Touch 2.x 
                                cmp = this.$getComponentFromElement(dom);
                            }
                        }
                    }
                }
            }
 
            return cmp;
        },
 
        $getComponentFromElement: function (node, limit, selector) {
            var target = Ext.getDom(node),
                cache = Ext.ComponentManager.all,
                depth = 0,
                topmost, cmpId, cmp;
 
            if (cache.map) {
                cache = cache.map;
            }
 
            if (typeof limit !== 'number') {
                topmost = Ext.getDom(limit);
                limit = Number.MAX_VALUE;
            }
 
            if (cache) {
                while (target && target.nodeType === 1 && depth < limit && target !== topmost) {
                    cmpId = target.getAttribute('data-componentid') || target.id;
 
                    if (cmpId) {
                        cmp = cache[cmpId];
 
                        if (cmp && (!selector || Ext.ComponentQuery.is(cmp, selector))) {
                            return cmp;
                        }
 
                        // Increment depth on every *Component* found, not Element 
                        depth++;
                    }
 
                    target = target.parentNode;
                }
            }
 
            return null;
        },
 
        getScroller: function() {
            var cmp = this.getComponent(),
                scroller = null,
                scrollMgr;
 
            if (cmp) {
                scrollMgr = cmp.scrollManager;
                if (scrollMgr) {
                    // 5.0 
                    scroller = scrollMgr.scroller;
                } else if (cmp.getScrollable) {
                    // 5.1+ 
                    scroller = cmp.getScrollable();
                }
            }
 
            return scroller;
        },
 
        /**
         * Returns the elements `textContent`.
         */
        getText: function () {
            var dom = this.dom;
 
            if (inputTags[dom.tagName]) {
                return dom.value;
            }
 
            return dom[ST.isIE8 ? 'innerText' : 'textContent'];
        },
 
        /**
         * Returns `true` if this element has any of the classes in the given `cls` string
         * (of space-separated classes) or array of class names. Array elements are not
         * checked for space-separators.
         *
         * @param {String/String[]} cls The classes to test
         * @return {Boolean}
         * @method hasAnyCls
         */
        hasAnyCls: function (cls) {
            return this.hasCls(cls, true);
        },
 
        /**
         * Returns `true` if this element has the classes in the given `cls` string (of
         * space-separated classes) or array of class names. Array elements are not
         * checked for space-separators.
         *
         * @param {String/String[]} cls The classes to test
         * @param {Boolean} [any] Pass `true` to only require that one of the classes
         * must be present, `false` (the default) will require that all classes be present.
         * @return {Boolean}
         * @method hasCls
         */
        hasCls: function (cls, any) {
            var classMap = this.getClassMap(),
                i, n;
 
            if (typeof cls === 'string') {
                cls = cls.split(spaceRe);
            }
 
            for (= n = cls && cls.length; i-- > 0; ) {
                if (classMap[cls[i]]) {
                    if (any) {
                        return true;  // one hit is good enough 
                    }
                } else if (!any) {
                    return false;   // one miss is good enough 
                }
            }
 
            // If we get here and !any then we only found matches. If any, we may 
            // not have found any matches if cls was empty. 
            return !any || !n;
        },
 
        isDetached: function () {
            var dom = this.dom; // logic borrow from Ext.isGarbage 
 
            // determines if the dom element is in the document or in the detached body element 
            // use by collectGarbage and Ext.get() 
            return dom &&
                // window, document, documentElement, and body can never be garbage. 
                dom.nodeType === 1 && dom.tagName !== 'BODY' && dom.tagName !== 'HTML' &&
                // if the element does not have a parent node, it is definitely not in the 
                // DOM - we can exit immediately 
                (!dom.parentNode ||
                    // If the element has an offset parent we can bail right away, it is 
                    // definitely in the DOM. 
                    (!dom.offsetParent &&
                        // if the element does not have an offsetParent it can mean the 
                        // element is either not in the dom or it is hidden. The next 
                        // step is to check to see if it can be found via getElementById 
                        dom.ownerDocument.getElementById(dom.id) !== dom
                    )
                );
        },
 
        isUserEditable: function() {
            var me = this,
                dom = me.dom,
                contentEditable = dom.contentEditable;
 
            // contentEditable will default to inherit if not specified, only check if the 
            // attribute has been set or explicitly set to true 
            // http://html5doctor.com/the-contenteditable-attribute/ 
            if ((inputTags[dom.tagName] && !nonEditableInputTypes[dom.type] &&
                !dom.readOnly && !me.hasAttribute('disabled')) ||
                (contentEditable === '' || contentEditable === 'true')) {
                return true;
            }
            return false;
        },
 
        isVisible: function () {
            var me = this,
                el = me.dom.parentNode,
                isVisible = me.getStyle('visibility') !== 'hidden' &&
                            me.getStyle('display') !== 'none';
 
            for ( ; isVisible && el && el.nodeType === 1; el = el.parentNode) {
                // css visibility is inherited so only need to check 'display' on ancestors 
                if (ST.fly(el, '$').getStyle('display') === 'none') {
                    // NOTE: we use a private fly since we are likely a fly ourselves 
                    isVisible = false;
                }
            }
 
            return isVisible;
        },
 
        hasAttribute: function(attribute) {
            var dom = this.dom,
                ret;
 
            if (dom.hasAttribute) {
                ret = dom.hasAttribute(attribute);
            } else {
                // IE8m 
                ret = dom.getAttribute(attribute) != null;
            }
 
            return ret;
        },
 
        on: function (eventName, fn, scope, capture) {
            return Element.on(this.dom, eventName, fn, scope, capture);
        },
 
        /**
         * @method query
         * Selects child nodes based on the passed CSS selector.
         * Delegates to document.querySelectorAll. More information can be found at
         * [http://www.w3.org/TR/css3-selectors/](http://www.w3.org/TR/css3-selectors/)
         *
         * All selectors, attribute filters and pseudos below can be combined infinitely
         * in any order. For example `div.foo:nth-child(odd)[@foo=bar].bar:first` would be
         * a perfectly valid selector.
         *
         * ## Element Selectors:
         *
         * * \* any element
         * * E an element with the tag E
         * * E F All descendant elements of E that have the tag F
         * * E > F or E/F all direct children elements of E that have the tag F
         * * E + F all elements with the tag F that are immediately preceded by an element with the tag E
         * * E ~ F all elements with the tag F that are preceded by a sibling element with the tag E
         *
         * ## Attribute Selectors:
         *
         * The use of @ and quotes are optional. For example, div[@foo='bar'] is also a valid attribute selector.
         *
         * * E[foo] has an attribute "foo"
         * * E[foo=bar] has an attribute "foo" that equals "bar"
         * * E[foo^=bar] has an attribute "foo" that starts with "bar"
         * * E[foo$=bar] has an attribute "foo" that ends with "bar"
         * * E[foo*=bar] has an attribute "foo" that contains the substring "bar"
         * * E[foo%=2] has an attribute "foo" that is evenly divisible by 2
         * * E[foo!=bar] has an attribute "foo" that does not equal "bar"
         *
         * ## Pseudo Classes:
         *
         * * E:first-child E is the first child of its parent
         * * E:last-child E is the last child of its parent
         * * E:nth-child(n) E is the nth child of its parent (1 based as per the spec)
         * * E:nth-child(odd) E is an odd child of its parent
         * * E:nth-child(even) E is an even child of its parent
         * * E:only-child E is the only child of its parent
         * * E:checked E is an element that is has a checked attribute that is true (e.g. a radio or checkbox)
         * * E:first the first E in the resultset
         * * E:last the last E in the resultset
         * * E:nth(n) the nth E in the resultset (1 based)
         * * E:odd shortcut for :nth-child(odd)
         * * E:even shortcut for :nth-child(even)
         * * E:not(S) an E element that does not match simple selector S
         * * E:has(S) an E element that has a descendant that matches simple selector S
         * * E:next(S) an E element whose next sibling matches simple selector S
         * * E:prev(S) an E element whose previous sibling matches simple selector S
         * * E:any(S1|S2|S2) an E element which matches any of the simple selectors S1, S2 or S3//\\
         *
         * ## CSS Value Selectors:
         *
         * * E{display=none} CSS value "display" that equals "none"
         * * E{display^=none} CSS value "display" that starts with "none"
         * * E{display$=none} CSS value "display" that ends with "none"
         * * E{display*=none} CSS value "display" that contains the substring "none"
         * * E{display%=2} CSS value "display" that is evenly divisible by 2
         * * E{display!=none} CSS value "display" that does not equal "none"
         *
         * @param {String} selector The CSS selector.
         * @param {Boolean} [asDom=false] `false` to return an array of ST.Element
         * @param single (private)
         * @return {HTMLElement[]/ST.Element[]} An Array of elements (
         * HTMLElement or ST.Element if `asDom` is `false`) that match the selector.  
         * If there are no matches, an empty Array is returned.
         */
        query: function(selector, asDom, single) {
            var dom = this.dom,
                results, len, node, nodes, i;
 
            if (!dom) {
                return null;
            }
 
            if (single) {
                // if single, only run querySelector 
                node = dom.querySelector(selector);
                return asDom ? node : ST.get(node);
            } else {
                // if not single, run the full QSA 
                results = [];
                nodes = dom.querySelectorAll(selector);
 
                for (= 0, len = nodes.length; i < len; i++) {
                    node = nodes[i];
                    results.push(asDom ? node : ST.get(node));
                }
 
                return results;
            }
        },
 
        /**
         * @method down
         * Selects a single child at any depth below this element based on the passed CSS selector (the selector should not contain an id).
         * @param {String} selector The CSS selector
         * @param {Boolean} [asDom=false] `true` to return the DOM node instead of ST.Element
         * @return {HTMLElement/ST.Element} The child ST.Element (or DOM node if `asDom` is `true`)
         */
        down: function (selector, asDom) {
            return this.query(selector, asDom, true);
        },
 
        /**
         * @method child
         * Selects a single *direct* child based on the passed CSS selector (the selector should not contain an id).
         * @param {String} selector The CSS selector.
         * @param {Boolean} [asDom=false] `true` to return the DOM node instead of ST.Element.
         * @return {HTMLElement/ST.Element} The child ST.Element (or DOM node if `asDom` is `true`)
         */
        child: function (selector, asDom) {
            var me = this,
                results = me.query(selector, true),
                len = results.length,
                i, node;
 
            for (i=0; i<len; i++) {
                node = results[i];
                // if the parentNode of this node matches our starting context,  
                // this is a match and we can stop checking 
                if (node.parentNode === me.dom) {
                    return asDom ? node : ST.get(node);
                }
            }
 
            return null;
        },
 
        /**
         * @method up
         * Walks up the dom looking for a parent node that matches the passed simple selector (e.g. 'div.some-class' or 'span:first-child').
         * This is a shortcut for findParentNode() that always returns an ST.Element.
         * @param {String} selector The simple selector to test.
         * @param {Number/String/HTMLElement/ST.Element} [limit]
         * The max depth to search as a number or an element that causes the upward
         * traversal to stop and is **not** considered for inclusion as the result.
         * (defaults to 50 || document.documentElement)
         * @param {Boolean} [asDom=false] True to return the DOM node instead of ST.Element
         * @return {ST.Element/HTMLElement} The matching DOM node (or DOM node if `asDom` is `true`)
         */
        up: function (selector, asDom, limit) {
            return this.findParentNode(selector, asDom, limit);
        },
 
        /**
         * @method is
         * Returns `true` if this element matches the passed simple selector
         * (e.g. 'div.some-class' or 'span:first-child').
         * @param {String/Function} selector The simple selector to test or a function which is passed
         * candidate nodes, and should return `true` for nodes which match.
         * @return {Boolean} `true` if this element matches the selector, else `false`.
         */
        is: function (selector) {
            var matchesSelector = this.matchesSelection(),
                dom = this.dom;
 
            if (matchesSelector) {
                return dom[matchesSelector](selector);
            } else {
                var elems = dom.parentNode.querySelectorAll(selector),
                    count = elems.length;
 
                for (var i = 0; i < count; i++) {
                    if (elems[i] === dom) {
                        return true;
                    }
                }
                return false;
            }
        },
        
        /**
         * @private
         */
        findParentNode: function (selector, asDom, limit) {
            var p = ST.fly(this.dom.parentNode);
            return p ? p.findParent(selector, asDom, limit) : null;
        },
 
        /**
         * @private
         */
        findParent: function(selector, asDom, limit) {
            var me = this,
                target = me.dom,
                topmost = document.documentElement,
                depth = 0;
 
            if (limit || limit === 0) {
                if (typeof limit !== 'number') {
                    topmost = ST.getDom(limit);
                    limit = Number.MAX_VALUE;
                }
            } else {
                // No limit passed, default to 50 
                limit = 50;
            }
 
            while (target && target.nodeType === 1 && depth < limit && target !== topmost) {
                if (ST.fly(target).is(selector)) {
                    return !asDom ? ST.get(target) : target;
                }
                depth++;
                target = target.parentNode;
            }
            return null;
        },
 
        /**
         * @private
         */
        matchesSelection: function () {
            var el = document.documentElement,
                w3 = 'matches',
                wk = 'webkitMatchesSelector',
                ms = 'msMatchesSelector',
                mz = 'mozMatchesSelector';
 
            return el[w3] ? w3 : el[wk] ? wk : el[ms] ? ms : el[mz] ? mz : null;
        },
 
        statics: {
            getViewportScrollElement: function() {
                var standard = this.$standardScrollElement,
                    el = doc.scrollingElement,
                    iframe, frameDoc, el;
 
                if (el) {
                    return el;
                }
 
                if (standard === undefined) {
                    iframe = document.createElement('iframe');
 
                    iframe.style.height = '1px';
                    document.body.appendChild(iframe);
                    frameDoc = iframe.contentWindow.document;
                    frameDoc.write('<!DOCTYPE html><div style="height:9999em">x</div>');
                    frameDoc.close();
                    standard = frameDoc.documentElement.scrollHeight > frameDoc.body.scrollHeight;
                    iframe.parentNode.removeChild(iframe);
 
                    this.$standardScrollElement = standard;
                }
                return standard ? docEl : body;
            },
 
            getViewportSize: function() {
                return {
                    height: window.innerHeight || docEl.clientHeight,
                    width: window.innerWidth || docEl.clientWidth
                }
            },
 
            on: function (el, eventName, fn, scope, capture) {
                var ieEventModel = (el.attachEvent && (navigator.msMaxTouchPoints == null)), // IE8/9 
                    wrap = function() {
                        return fn.apply(scope, arguments);
                    };
 
                fn = (typeof fn === 'string') ? scope[fn] : fn;
 
                if (ieEventModel) {
                    el.attachEvent('on' + eventName, wrap);
                } else {
                    el.addEventListener(eventName, wrap, !!capture);
                }
 
                return {
                    destroy: function() {
                        if (ieEventModel) {
                            el.detachEvent('on' + eventName, wrap);
                        } else {
                            el.removeEventListener(eventName, wrap, !!capture);
                        }
                        wrap = null;
                    }
                };
            }
        }
    };
},
function (Element) {
    var flies = {};
 
    /**
     * Given one of the various ways to identify a DOM node, this method returns the
     * DOM node or `null`.
     * @param {String/HTMLElement/ST.Element/Ext.Element} domNode
     * @return {HTMLElement}
     * @method getDom
     * @member ST
     */
    ST.getDom = function (domNode) {
        if (!domNode) {
            return null;
        }
 
        // This piece is aligned with Ext.getDom() except that we check for nodeType 
        // to ensure we are not given truthy garbage. 
        // 
        if (typeof domNode === 'string') {
            domNode = document.getElementById(domNode);
        }
        else if (domNode.dom) {
            // May be an ST.Element or an Ext.Element, either way, just use the "dom" 
            // value. This ensures an ST.Element is returned. 
            domNode = domNode.dom;
        }
        else if (domNode != window && typeof domNode.nodeType !== 'number') {
            return null;
        }
 
        return domNode;
    };
 
    /**
     * Given one of the various ways to identify a DOM node, this method returns a
     * temporary, fly-weight `ST.Element` or `null`. Instances returned by this
     * method should be used briefly to call the methods of the `ST.Element` and then
     * ignored since the instance will be re-used by future calls to this method.
     * @param {String/HTMLElement/ST.Element/Ext.Element} domNode
     * @param {String} [flyName="fly"] An optional name for the fly. Passing a custom
     * name can be used to control the scope of re-use of the returned instance.
     * @return {ST.Element}
     * @method fly
     * @member ST
     */
    ST.fly = function (domNode, flyName) {
        if (!(domNode = ST.getDom(domNode))) {
            return null;
        }
 
        flyName = flyName || 'fly';
 
        var fly = flies[flyName] || (flies[flyName] = new Element());
 
        fly.dom = domNode;
        return fly;
    };
 
    /**
     * Given one of the various ways to identify a DOM node, this method returns a
     * temporary, fly-weight `ST.Element` or `null`. Each call to this method returns
     * a new `ST.Element` instance. Unlike `Ext.get()` this method does not maintain an
     * element cache nor does it assign an `id` to the element. In other words, this
     * method is equivalent to `new ST.Element()`.
     * @param {String/HTMLElement/ST.Element/Ext.Element} domNode
     * @return {ST.Element}
     * @method get
     * @member ST
     */
    ST.get = function (domNode) {
        if (!(domNode = ST.getDom(domNode))) {
            return null;
        }
 
        return new Element(domNode);
    };
});