/**
 * @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.event.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+)\])?/,
        atPathRe: /^@/,
        xpathRe: /^\//,
 
        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"]
         * currently only used for composite queries
         */
        find: function (target, wrap, root, direction) {
            var el;
 
            if (typeof target === 'function') {
                el = target();
            }
            else if (Locator.atPathRe.test(target)) {
                el = Locator.atPath(target);
            }
            else if (Locator.xpathRe.test(target)) {
                el = Locator.xpath(target);
            }
            else {
                el = Locator.composite(target, root, direction);
            }
 
            if (el && wrap) {
                el = new ST.Element(el);
            }
 
            return el;
        },
 
        atPath: function (path) {
            var parts = path.split('/'),
                regex = Locator.tagPathRegEx,
                i, n, m, count, tag, child,
                el = Locator.root.document;
 
            el = (parts[0] == '@') ? el.body
                : el.getElementById(parts[0].substring(1)); // remove '@' 
 
            for (= 1, n = parts.length; el && i < n; ++i) {
                m = regex.exec(parts[i]);
                count = m[2] ? parseInt(m[2], 10) : 1;
                tag = m[1].toUpperCase();
 
                for (child = el.firstChild; child; child = child.nextSibling) {
                    if (child.tagName == tag) {
                        if (count == 1) {
                            break;
                        }
                        --count;
                    }
                }
 
                el = child;
            }
 
            return el;
        },
 
        composite: function (target, queryRoot, direction) {
            var me = this,
                context = me._parseRoot(queryRoot),
                compContext = context.comp,
                elContext = context.el,
                root = context.root,
                parsedTarget = me._parseSelector(target, context.type),
                domQuery = parsedTarget.domQuery,
                compQuery = parsedTarget.compQuery,
                isComposite = parsedTarget.isComposite,
                direction = direction || 'down',
                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);
            } 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);  
 
                    root = comp && (comp.el || comp.element);
                    root = root && root.dom;
                } else if (!domQuery) {
                    root = null;
                }
 
                if (domQuery && root) {
                    return me._doDomQuery(domQuery, root, direction);
                }
            }
 
            return root;
        },
 
        xpath: function (target) {
            var doc = Locator.root.document,
                res = doc.evaluate(target, doc, null, 5, null), // ORDERED_NODE_ITERATOR_TYPE 
                el = res ? res.iterateNext() : null;
 
            if (el && ST.options.failOnMultipleMatches && res.iterateNext()) {
                throw new Error('XPath locator matches multiple items: "' + target + '"');
            }
 
            return el;
        },
 
        /**
         * @private
         * Executes composite query for hierarchy requests
         * @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) {
            var me = this,
                start = start || root,
                originalRoot, el, matches, match, comp,
                i, node, verified, rootEl, parent;
 
            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);
                    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) {
            var target, parent, matches, i;
 
            root = ST.get(root);
            // run the appropriate method against the passed context element            
            return root[direction](selector, true);
        },
 
        _doComponentQuery: function (selector, root, direction) {
            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);
                        break;
                    case 'child':
                        comp = root.child && root.child(selector);
                        break;
                }
            }
            // otherwise, fall back to component query 
            else {
                comp = Ext.ComponentQuery.query(selector);
 
                if (comp && comp.length) {
                    if (comp.length > 1 && ST.options.failOnMultipleMatches) {
                        throw new Error('Component Query locator matches multiple items: "'
                                + selector + '"');
                    }
 
                    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
                };
 
            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) {
                        result.comp = root.cmp || null;
                        result.el = root.el || null;
                    }
                } 
                // 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;
        },
 
        _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
 */
ST.find = ST.Locator.find;