/**
 * @class ST.Locator
 * @singleton
 *
 * Sencha Test provides multiple ways to locate an element from a text string. The best
 * and most reliable way to locate elements will be application-specific, so Sencha Test
 * generalizes the tools needed in what are called "locators".
 *
 * A locator solves the same probelm as a CSS selector but is a super-set of CSS selector
 * syntax. The locator syntax is more expressive than selectors to provide more options
 * for testing real-world applications.
 *
 * When testing applications, ideally the application developers provide a reliable way
 * for testers to locate application components and elements.
 *
 * Locators appear in the {@link ST.playable.Playable#target target} property of records
 * passed to {@link ST#play}. Locators can be passed to {@link ST#find} to find an
 * {@link ST.Element element}. Locators are also passed to {@link ST#element} to create
 * {@link ST.future.Element future elements} and {@link ST#component} to create
 * {@link ST.future.Component future components}.
 *
 * ## Locating Elements
 *
 * ### At-Path
 * Locators that start with the "@" character are called "at-paths". The first token of
 * an at-path is an element ID. Following the first token is a slash-delimited sequence
 * of tag names and offsets, similar to XPath. For example:
 *
 *      @some-div/span[2]
 *
 * This identifies the 2nd "span" element that is an immediate child of the element with
 * the id "some-div". The equivalent XPath expression would be:
 *
 *      //[@id="some-div"]/span[2]
 *
 * The primary advantages of at-paths over XPath are compactness and speed. This is because
 * an at-path uses `getElementById` followed by a simple path based on tag names. Because
 * at-paths are inherently based on ID's, they will be most useful in applications that
 * assign meaningful ID's to their components.
 *
 * ### XPath
 * XPath is probably the most powerful supported locator syntax. Sencha Test uses the
 * [document.evaluate](http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html#XPathEvaluator-evaluate)
 * method of the browser, but also a [polyfill](https://github.com/google/wicked-good-xpath)
 * when this method is not present.
 *
 * In addition to attribute matching, XPath can also navigate upwards, unlike CSS
 * selectors. For example:
 *
 *      //[id="some-div"]/..
 *
 * The above XPath selects the parent node of the node having ID of "some-div".
 *
 * **IMPORTANT** Sencha Test requires that all XPath locators start with a slash character.
 * Typically XPath locators will begin with "//" (as shown above) so that matches do not
 * start at the document root.
 *
 * Some useful resources on XPath:
 *
 *   * [DOM XPath Specification](http://www.w3.org/TR/DOM-Level-3-XPath/xpath.html)
 *   * [XPath and CSS Selectors](http://ejohn.org/blog/xpath-css-selectors/)
 *
 * ### DOM Query
 * The DOM Query, or CSS Selector, is perhaps the most familiar locator syntax supported
 * by Sencha Test. To differentiate DOM Query locators from the Component and Composite
 * Queries (discussed below), a DOM Query starts with ">>" or "=>".
 *
 * The above paths would be approximated by the following DOM Query:
 *
 *      >> #some-div > span:nth-child(2)
 *
 * This is only approximately the same because `nth-child()` does not require the first
 * child to also be a `span`.
 *
 * ## Locating Components
 * When testing applications built using Sencha frameworks (Ext JS and Sencha Touch), the
 * majority of logic operates at a layer above elements: Components. It is therefore more
 * desirable to locate and operate on components than raw DOM elements.
 *
 * ### Component Query
 * "[Component Query](http://docs.sencha.com/extjs/6.0/6.0.1-classic/#!/api/Ext.ComponentQuery)"
 * is a feature provided by Sencha frameworks that can locate components of the application.
 * Component Query syntax is essentially the same as DOM Query.
 *
 * Consider:
 *
 *      #some-component
 *
 * The above will locate a Component with an `id` or `itemId` property of "some-component".
 *
 * ### Composite Query
 * Finally, you can combine Component Query and DOM Query in a "Composite Query" by using
 * the "=>" to separate the two pieces.
 *
 * For example:
 *
 *      #some-component => div.foo
 *
 * This locates the child "div" with class "foo" inside the component with `id` (or `itemId`)
 * of "some-component".
 */
ST.Locator = (function (Locator) {
    return ST.apply(Locator, {
        tagPathRegEx: /([\w-:]+)(?:\[(\d+)\])?/,
        attrRegEx: /\[(.*?)\]/g,
        atPathRe: /^@/,
        xpathRe: /^\/|\.\//, // need to allow for /, //, and .// (current context)
 
        root: window,
 
        /**
         * Given the `target` locator string, return the matching element or `null` if one
         * is not found. See {@link ST.Locator} for a description of valid locator
         * strings.
         *
         * See also the short-hand equivalent {@link ST#find}.
         *
         * @param {String/Function} target The target locator string or a function that
         * returns the DOM node.
         * @param {Boolean} [wrap] Pass `true` to return a wrapped {@link ST.Element}
         * instead of the raw DOM node.
         * @param {HTMLElement/ST.Element/Ext.Component} [root]
         * @param {"down"/"up"/"child"/"sibling?"} [direction="down"]
         * @param {Boolean} [allowMultiples=undefined] Pass `true` to return multiple match results if available.
         * currently only used for composite queries
         * @param {Boolean} [returnComponents=undefined] Pass `true` to return results as Components instead of
         * ST.Element (if possible). Only applicable if the target is a standard Component Query.
         * @return {HTMLElement/HTMLElement[]/ST.Element/ST.Element[]}
         */
        find: function (target, wrap, root, direction, allowMultiples, returnComponents) {
            var opts = { $isOpts: true },
                result = [],    
                el, scope;
 
            if (root) { 
                opts.root = root;
            }
 
            if (direction) { 
                opts.direction = direction;
            }
          
            if (allowMultiples) { 
                opts.allowMultiples = allowMultiples;
            }
          
            if (returnComponents) { 
                opts.returnComponents = returnComponents;
            }
            
            if (target == null && root == null) {
                return null;
            }
 
            if (typeof target === 'number') {
                el = ST.cache.get(target);
            } else if (typeof target === 'object' && root == null) {
                el = target;
            } else if (typeof target === 'function') {
                scope = target.$scope || window;
                el = target.apply(scope);
            }
            else if (Locator.atPathRe.test(target)) {
                el = Locator.atPath(target, opts);
            }
            else if (Locator.xpathRe.test(target)) {
                el = Locator.xpath(target, opts);
            }
            else {
                el = Locator.composite(target, opts);
            }
 
            if (el && wrap) {
                if (ST.isArray(el)) {
                    for (var i in el) {
                        if (el.hasOwnProperty(i)) {
                            if (returnComponents && (el[i].isComponent || el[i].isWidget)) {
                                result.push(el[i]);
                            } else {
                                result.push(new ST.Element(el[i]));
                            }
                        }
                    }
                    return result;
                } else {
                    // TODO: Unless returnComponents=true, el should *always* be a dom node...
                    if (!el.isComponent && !el.isWidget) {
                        el = new ST.Element(el);
                    }
                }
            }
 
            return el;
        },
 
        /**
         * @return {ST.Element[]/Ext.Component[]}
         */
        findAll: function (target, wrap, root, direction, returnComponents) {
            var results = this.find(target, wrap, root, direction, true, returnComponents);
            
            if (results === null) {
                results = [];
            } else if (!ST.isArray(results)) { 
                results = [results];
            }
            
            return results;
        },
        
        atPath: function (path, opts) {
            var parts = path.split('/'),
                regex = Locator.tagPathRegEx,
                attrRe = Locator.attrRegEx,
                i, n, m, x, count, tag, child,
                matchedAttr, attrs, attr, key, value, hasInvalidAttr,
                el = Locator.root.document;
 
            el = (parts[0] == '@') ? el.body
                : el.getElementById(parts[0].substring(1)); // remove '@'
 
            for (i = 1, n = parts.length; el && i < n; ++i) {
                m = regex.exec(parts[i]);
                attrs = [];
                count = m[2] ? parseInt(m[2], 10) : 1;
                tag = m[1].toUpperCase();
 
                while ((matchedAttr = attrRe.exec(parts[i])) !== null) {
                    attrs.push(matchedAttr[1]);
                }
 
                for (child = el.firstChild; child; child = child.nextSibling) {
                    if (child.tagName == tag) {
                        if (count == 1) {
                            // if we have attributes...                            
                            if (attrs && attrs.length) {
                                // start with a blank slate
                                hasInvalidAttr = false;
 
                                for (x = 0; x < attrs.length; x++) {
                                    attr = attrs[x];
                                    // if it's a index, skip
                                    if (!isNaN(parseInt(attr))) {
                                        continue;
                                    }
                                    // if it's a key/value pair
                                    else if (attr.search(/=/) !== -1) {
                                        attr = attr.split('=');
                                        key = attr[0];
                                        value = attr[1].replace(/^["|'](.*)["|']$/g, '$1');
 
                                        if (child.getAttribute(key) != value) {
                                            hasInvalidAttr = true;
                                        }
                                    }
                                    // if it's a contains() declaration
                                    else if (attr.search(/contains\(/)) {
 
                                    }
                                }
 
                                // TODO: selector is broken; default to original element?
                                if (hasInvalidAttr) {
                                    child = el;
                                }
                            }
 
                            break;
                        }
                        --count;
                    }
                }
 
                el = child;
            }
 
            if (opts && opts.returnComponents) {
                var fly = ST.fly(el),
                    cmp = fly && fly.getComponent();
                
                return cmp || el;
            } else {
                return el;
            }
        },
 
        composite: function (target, queryRoot, direction, allowMultiples, returnComponents) {
            var me = this,
                opts = queryRoot,    
                isOpts = typeof opts === 'object' && opts.$isOpts,
                queryRoot = isOpts ? opts.root : queryRoot,
                direction = isOpts ? opts.direction : direction,
                allowMultiples = isOpts ? opts.allowMultiples : allowMultiples,
                returnComponents = isOpts ? opts.returnComponents : returnComponents,
                context = me._parseRoot(queryRoot),
                compContext = context.comp,
                root = context.root,
                parsedTarget = me._parseSelector(target, context.type),
                domQuery = parsedTarget.domQuery,
                compQuery = parsedTarget.compQuery,
                isComposite = parsedTarget.isComposite,
                direction = direction || 'down',
                results = [],
                comp;
 
            // if this is a composite query of the "up" or "child" variety
            if (isComposite && compContext && direction !== 'down') {
                return me._doCompositeQuery(compQuery, domQuery, compContext, direction, null, allowMultiples);
            } else {
                // if we failed to resolve the requested root, we can skip the rest
                if (context.failedRoot) {
                    return null;
                }
                // if a component query was detected, run first to determine root
                if (compQuery) {
                    root = compContext || null;
 
                    comp = me._doComponentQuery(compQuery, root, direction, allowMultiples);
 
                    if (ST.isArray(comp)) {
                        if (returnComponents) {
                            root = comp;
                        } else {
                            root = [];
                            for (var i in comp) {
                                if (comp.hasOwnProperty(i)) {
                                    root[i] = comp[i] && (comp[i].el || comp[i].element);
                                    root[i] = root[i] && root[i].dom;
                                }
                            }
                        }
                    } else {
                        if (returnComponents) {
                            root = comp;
                        } else {
                            root = comp && (comp.el || comp.element);
                            root = root && root.dom;
                        }
                    }
                } else if (!domQuery) {
                    root = null;
                }
 
                if (domQuery && root) {
                    if (allowMultiples && ST.isArray(root)) {
                        for (var i in root) {
                            if (!root.hasOwnProperty(i)) continue
                            // TODO test _doDomQuery() returning mulitple results
                            results.push(me._doDomQuery(domQuery, root[i], direction, allowMultiples));
                        }
                        return results;
                    } else {
                        return me._doDomQuery(domQuery, root, direction, allowMultiples);
                    }
                }
            }
 
            return root;
        },
 
        xpath: function (target, opts) {
            var opts = opts || {},
                // allow context switch for query (useful for iframes and different windows)    
                doc = opts.context || Locator.root.document,
                // allow root switch based on configuration (nice for subqueries of existing, located elements)
                root = doc, //root = opts.root || doc,  // opts.root was an invalid Node type - needs to be a dom node, not an ST locator object
                // root could be an ST.Element instance
                root = root && root.dom ? root.dom : root,
                // do the query!!!
                res = doc.evaluate(target, root, null, 5, null), // ORDERED_NODE_ITERATOR_TYPE
                el = res ? res.iterateNext() : null,
                allowMultiples = opts.allowMultiples,
                results, i, fly, cmp;
 
            if (allowMultiples) {
                results = el ? [el] : [];
            } else { 
                results = el || null;
            }
            
            if (el) { 
                if (allowMultiples) {
                    while (el = res.iterateNext()) {
                        results.push(el);
                    }
                } else if (ST.options.failOnMultipleMatches && res.iterateNext()) { 
                    e = new Error('XPath locator matches multiple items: "' + target + '"');
                    e.multipleMatches = true;
                    throw e;
                }
            }
 
            if (opts && opts.returnComponents) {
                for (i in results) {
                    fly = ST.fly(results[i]);
                    cmp = fly && fly.getComponent();
                    
                    if (cmp) {
                        results[i] = cmp;
                    }
                }
            }
 
            return results;
        },
 
        /**
         * @private
         * Executes composite query for hierarchy requests of the 'up' and 'child' varieties
         * @param {String} compQuery The Component query
         * @param {String} domQuery The DOM query
         * @param {Ext.Component} root The root component
         * @param {String} direction The direction of the hierarchical query.
         * @param {String} start The id of the original starting context
         * @return {HTMLElement}
         */
        _doCompositeQuery: function (compQuery, domQuery, root, direction, start, allowMultiples) {
            var me = this,
                start = start || root,
                originalRoot, el, matches, match, comp,
                i, node, verified, rootEl;
 
            if (direction === 'up') {
                // first, run component query side of composite
                comp = root.up(compQuery);
 
                if (comp) {
                    // we have a match, so run dom query portion
                    el = comp.el || comp.element;
                    match = ST.fly(el).down(domQuery, true /* asDom */);
                    verified = false;
 
                    if (match) {
                        // Here's where it gets interesting; we found a match, but it could be *anywhere* in the hierarchy
                        // of the matched component; so, we need to work our way from the bottom (original node) to the top (matched component)
                        // If we find the matched node along the way, this is a valid ancestor element
                        // If we don't find the matched node, we need to continue up() the component tree, as a higher level might have
                        // what we're after
                        originalRoot = start;
                        // the rootEl is where the query started
                        rootEl = originalRoot.el || originalRoot.element;
 
                        // ensure that the match isn't our starting location!
                        if (rootEl.dom !== match) {
                            node = rootEl.dom;
                        }
 
                        // verify that this match is *above* the original root in the dom hierarchy
                        while (node) {
                            if (node === match) {
                                // matching node has been located
                                node = null;
                                verified = true;
                            } else if (node !== el.dom) {
                                // node not found yet, keep interating up the hierarchy
                                node = node.parentNode;
                            } else {
                                // we're reached the top-most node where a match is allowed without finding anything; abort
                                node = null;
                            }
                        }
                    }
                    // if we couldn't verify the match, we need to continue the search upward, recursively
                    if (!verified) {
                        match = me._doCompositeQuery(compQuery, domQuery, comp, direction, start);
                    }
                }
 
            } else if (direction === 'child' && root.query) {
                // for first, last, and child, we can query for matches on the component level
                rootEl = root.el || root.element;
                matches = root.query(compQuery);
 
                // now we'll loop over the matches, and run the appropriate dom query method on the component's root element
                for (i = 0; i < matches.length; i++) {
                    comp = matches[i];
                    el = comp.el || comp.element;
                    match = ST.get(el).down(domQuery, true);
                    // if we have a match, verify that the element is actually a child of the original root
                    if (match && (match.parentNode === rootEl.parentNode) && (rootEl !== match)) {
                        break;
                    } else {
                        match = null;
                    }
                }
            }
 
            return match || null;
        },
 
        _doDomQuery: function (selector, root, direction, allowMultiples) {
            root = ST.get(root);
            // run the appropriate method against the passed context element           
            return root[direction](selector, true, (typeof allowMultiples === 'undefined') ? null : !allowMultiples /* single */);
        },
 
        _doComponentQuery: function (selector, root, direction, allowMultiples) {
            var comp;
 
            // if we have a root, it's a contextual query so we can use direction
            if (root) {
                switch (direction) {
                    case 'up':
                        comp = root.up(selector);
                        break;
                    case 'down':
                        comp = root.down && root.down(selector, false /* asDom */, !allowMultiples /* single */);
                        break;
                    case 'child':
                        comp = root.child && root.child(selector, false /* asDom */, !allowMultiples /* single */);
                        break;
                }
            }
            // otherwise, fall back to component query
            else {
                comp = Ext.ComponentQuery.query(selector);
 
                if (comp && comp.length) {
                    if (comp.length > 1 && ST.options.failOnMultipleMatches && !allowMultiples) {
                        throw new Error('Component Query locator matches multiple items: "'
                            + selector + '"');
                    }
                    
                    if (!allowMultiples) {
                        comp = comp[0];
                    }
                } else {
                    comp = null;
                }
            }
 
            return comp || null;
        },
 
        _parseRoot: function (root) {
            var defaultRoot = Locator.root.document,
                type = typeof root,
                result = {
                    comp: null,
                    el: null,
                    type: 'element',
                    failedRoot: false,
                    root: defaultRoot
                },
                contextCmp = null,
                contextEl = null,
                context;
 
            if (!root || root === defaultRoot) {
                result.type = null;
            } else {
                if (root && type === 'object') {
                    // is an Ext JS / Sencha Touch component?
                    if (root.isComponent) {
                        result.comp = root;
                        result.el = root.el || root.element;
                    }
                    // this is either an Ext JS element or an ST.Element
                    else if (root.dom) {
                        result.el = root;
                    }
                    // this could be an html element
                    else if (root.nodeType && root.nodeType === 1) {
                        result.el = ST.get(root);
                    }
                    // this may be a future
                    else if (root.$ST) {
                        context = root;
 
                        if (context) {
                            contextCmp = context.cmp || null;
                            contextEl = context.el || null;
                        }
                        result.comp = contextCmp;
                        result.el = contextEl;
                    }
                    // this is a locator chain
                    else if (ST.isArray(root)) {
                        // _parseChain will ultimately produce the "result" that _parseRoot produces for other scenarios
                        context = this._parseChain(root);
                        return context;
                    }
                }
                // if it's a string, we'll try to produce an ST.Element from it
                else if (type === 'string') {
                    result.el = ST.get(root);
                }
 
                // determine the correct type
                if (result.comp) {
                    result.type = 'component';
                } else {
                    result.type = result.el ? 'element' : null;
                }
                // if element was located, use it for the root
                if (result.el) {
                    result.root = result.el.dom || result.el;
                } else {
                    result.root = null;
                    result.failedRoot = true;
                }
            }
 
            return result;
        },
 
        _parseChain: function (chain) {
            var len = chain.length - 1,
                i, item, root, future;
 
            // The goal here is to loop over the "locator chain" that was provided on the originating future;
            // we'll use this descriptor to walk the identified hierarchy so that we can recreate it in order
            // to locate the desired element/component, starting from the correct root. 
            // This allows the locator to be remoted since we don't have to have any extant futures (or their corresponding)
            // components/elements; instead, we can simply re-walk the hierarchy on demand
            for (i = 0; i < len; i++) {
                item = chain[i];
 
                if (item.locator && item.locator === '$$SKIP$$') {
                    root = root;
 
                    if (root && root.isComponent && item.type === 'element') {
                        root = root.element || root.el;
                    }
                } else if (ST.playable.Playable.hasLocatorFn(item)) {
                    // call locator fn of future
                    future = ST.clsFromString(item.futureClsName + '.prototype.playables.' + ST.capitalize(item.type));
                    // pass current root and args to the locatorFn
                    root = future.prototype.locatorFn(root, item.args);
                } else if (item.el) {
                    root = item.el;
                } else {
                    // the root will change on each iteration, so we'll re-find() based on the new root
                    root = this.find(item.locator, false, root, item.direction);
                }
                // if the type is component, we'll try to resolve the component
                // so we can use it as the root
                if (root && item.futureClsName && ST.future.Element.isFutureType(item.futureClsName, 'component')) {
                    root = ST.fly(root).getComponent();
                }
            }
 
            // Here we're at the end of the line. Our root could be an element or a component.
            // Since _parseRoot() already deals in these scenarios, we'll just run through it once more 
            // This will produce the correct result that our original find() is needing in order to continue
            return this._parseRoot(root);
        },
 
        _parseSelector: function (selector, contextType) {
            var Ext = ST.Ext,
                hasCQ = Ext && Ext.ComponentQuery,
                selector = selector || (contextType ? '*' : ''),
                compPos = selector.indexOf('=>'),
                domPos = selector.indexOf('>>'),
                parts, result;
 
            result = {
                domQuery: null,
                compQuery: null,
                isComposite: false,
                selector: selector
            }
 
            // context is a component
            if (hasCQ) {
                if (contextType !== 'element') {
                    // no fat arrow (>> .my-class)
                    if (domPos === 0) {
                        result.domQuery = ST.String.trim(selector.replace('>>', ''));
                    }
                    // fat arrow at beginning (=> .my-class)
                    else if (compPos === 0) {
                        result.domQuery = ST.String.trim(selector.replace('=>', ''));
                    }
                    // fat arrow between selectors (container => .my-class)
                    else if (compPos > 0) {
                        parts = selector.split('=>');
                        result.domQuery = ST.String.trim(parts[1]);
                        result.compQuery = ST.String.trim(parts[0]);
                        result.isComposite = true;
                    }
                    // pure component query (container)
                    else {
                        result.compQuery = ST.String.trim(selector)
                    }
                } else if (domPos === -1 || domPos === 0) {
                    if (compPos !== -1) {
                        throw new Error('The specified composite query ("' + selector + '") cannot be used in the current context');
                    }
 
                    result.domQuery = ST.String.trim(selector.replace('>>', ''));
                }
            }
            // if component query isn't an option, everything has to funnel through dom query
            else {
                result.domQuery = ST.String.trim(selector.replace('>>', ''));
            }
 
            return result;
        }
    });
})({});
 
wgxpath.install();  // polyfill win.document.evaluate for old browsers
 
/**
 * Given the `target` locator string, return the matching element or `null` if one is not
 * found. See {@link ST.Locator} for a description of valid locator strings.
 *
 * Alias for {@link ST.Locator#find}.
 *
 * @param {String} target The target locator string.
 * @param {Boolean} [wrap=false] Pass `true` to return a wrapped {@link ST.Element}
 * instead of the raw DOM node.
 * @param {HTMLElement/ST.Element/Ext.Component} [root]
 * @param {"down"/"up"/"child"} [direction="down"]
 * @method find
 * @member ST
 * @return {ST.Element}
 */
ST.find = ST.Locator.find;