/**
 * @class ST.locator.Strategy
 * This class is the default locator strategy. This can also be used as a base class for
 * other locator strategies. To register custom locator strategies, see
 * {@link ST#addLocatorStrategy} or {@link ST#setLocatorStrategies}.
 */
ST.locator.Strategy = ST.define({
    // TODO refactor target hunting methods to return targets found, not pass around arrays 
    ignoreElIdRe: /^ext-(?:gen|element)(?:\d+)$/,
    validIdRe: /^[a-z_][a-z0-9\-_]*$/i,
    autoGenIdRe: /^ext\-/,
    includeFullLocator: ST.apply(ST.apply({},
        ST.event.Event.keyEvents),
        ST.event.Event.focusEvents),
 
    constructor: function (config) {
        var me = this;
 
        ST.apply(me, config);
 
        me.locate = me._initter;  // hook the first call to locate() 
    },
 
    /**
     * Initializes this instance. This method is called immediately prior to the first
     * call to `locate`. This is done in a deferred manner to ensure that any necessary
     * code has been loaded (such as Ext JS).
     *
     * @since 1.0.1
     * @protected
     */
    init: function () {
        var me = this,
            x = ST.Ext,
            cssIgnore = me.cssIgnore,
            cssIgnorePat;
 
        // Copy off the prototype 
        me.cssIgnore = cssIgnore = ST.apply({}, cssIgnore);
 
        x = x && x.baseCSSPrefix;
        cssIgnorePat = me.getCssIgnorePatterns(x);
 
        if (cssIgnorePat.length) {
            me.cssIgnoreRe = new RegExp(cssIgnorePat.join('|'));
        }
 
        if (x) {
            ST.apply(cssIgnore, ST.Array.toMap([
                x + 'body',
 
                x + 'box-item',
                x + 'box-target',
 
                x + 'btn-inner',
                x + 'btn-wrap',
 
                x + 'component',
                x + 'fa',
                x + 'fit-item',
                x + 'form-field',
                x + 'grid-cell-inner',
                x + 'noicon'
            ]));
        }
    },
 
    _initter: function () {
        var me = this;
 
        delete me.locate;
 
        me.init();
 
        return me.locate.apply(me, arguments);
    },
 
    /**
     * This method should append target locators to the `targets` array based on the
     * provided `el`. Each identified target should be appended to the `targets` array
     * (e.g., using `push()`).
     *
     * Because a locator can describe the passed `el` or a parent node, results added to
     * the `targets` array should be an array consisting of the element and its locator.
     * For example:
     *
     *      if (el.id) {
     *          targets.push([ el, '@' + el.id ]);
     *      }
     *
     * @param {HTMLElement} el The element for which to generate target locator(s).
     * @param {Array[]} targets The array to which to append targets and their locator(s)
     * as an array of `[ el, locator ]`.
     * @param {Object} ev 
     * @return {Boolean} Returns `true` if any `targets` were generated.
     * @method locate
     */
    locate: function (el, targets, ev) {
        var me = this,
            ExtJS = ST.Ext,
            good = false,
            c, cmp, fly;
 
        if (ExtJS && ExtJS.ComponentQuery) {
            fly = ST.fly(el);
            cmp = fly && fly.getComponent();
 
            for (= cmp; c; c = c.getRefOwner && c.getRefOwner()) {
                if (!me.ignoreCmp(c)) {
                    if (me.getCQ(c, el, targets, ev)) {
                        me.processCustomBuilder(c, el, targets, ev);
                        good = true;
                    }
                    break;
                }
            }
        }
 
        if (me.getAtPath(el, targets)) {
            good = true;
        }
 
        return !!good;
    },
 
    getAtPath: function (el, targets) {
        if (!el) {
            el = document.body;
        }
 
        var me = this,
            good = false,
            path = [],
            stopper = (el.ownerDocument || el).body,
            count, sibling, t, tag;
 
        for (= el; t; t = t.parentNode) {
            if (== stopper) {
                path.unshift('@');
                good = true;
                break;
            }
            if (t.id && t.id.indexOf('/') < 0 && !me.ignoreElIdRe.test(t.id)) {
                path.unshift('@' + t.id);
                good = true;
                break;
            }
 
            for (count = 1, sibling = t; sibling = sibling.previousSibling; ) {
                if (sibling.tagName == t.tagName) {
                    ++count;
                }
            }
 
            tag = t.tagName && t.tagName.toLowerCase();
            if (tag) {
                if (count < 2) {
                    path.unshift(tag);
                } else {
                    path.unshift(tag + '[' + count + ']');
                }
            }
            else if (t.window == t) { // must use == for IE8 (from Ext.dom.Element) 
                break;
            }
        }
 
        if (targets && good) {
            targets.push([t, path[0]]);
 
            if (path.length > 1) {
                targets.push([el, path.join('/')]);
            }
        }
 
        return good;
    },
 
    /**
     * Generates a set of ComponentQuery candidates for the given Component. The generated
     * CQ selectors are "shallow" in that they do not describe the containment hierarchy
     * of the component.
     *
     * @param {Ext.Component} cmp
     * @param {HTMLElement} el The actual element to target. If this parameter is not
     * `null` this method may include an additional DOM query on the generated selectors
     * separated by "=>" (a "Composite Query").
     * @param {Array[]} targets The array to which to append targets and their locator(s)
     * as an array of `[ el, locator ]`.
     * @param {Object} ev 
     * @return {Boolean} Returns `true` if any `targets` were generated.
     * @since 1.0.1
     */
    getCQ: function (cmp, el, targets, ev) {
        var me = this,
            configList = me.configList,
            len = configList.length,
            good = false,
            n = targets && targets.length,
            i, item, k, sel, xtype;
 
        for (= 0; i < len; ++i) {
            if (me.getCQForProperty(cmp, configList[i], targets)) {
                good = true;
            }
        }
 
        if (!good) {
            xtype = me.getXType(cmp);
 
            if (xtype) {
                if (targets) {
                    targets.push([ cmp.el, xtype ]);
                }
 
                good = true;
            }
        }
 
        if (targets && el && n < (= targets.length)) {
            sel = me.getItemSelector(cmp, el, ev);
 
            if (sel) {
                item = sel[0];
                sel = ' => ' + sel[1];
 
                for (; n < k; ++n) {
                    targets[n][0] = item;
                    targets[n][1] += sel;
                }
            }
        }
 
        return good;
    },
 
    /**
     * Generates a ComponentQuery selector for the given Component using the specified
     * config property. The selector is "shallow" in that they do not describe the
     * containment hierarchy of the component.
     *
     * The supported properties are listed in the `configList` array.
     *
     * @param {Ext.Component} cmp
     * @param {String} prop The property to use in the generated selector.
     * @param {Array[]} targets The array to which to append targets and their locator(s)
     * as an array of `[ el, locator ]`.
     * @return {Boolean} Returns `true` if any `targets` were generated.
     * @since 1.0.1
     */
    getCQForProperty: function (cmp, prop, targets) {
        var extractor = this.extractors[prop],
            good = false;
 
        if (extractor) {
            good = extractor.call(this, cmp, targets);
        }
 
        return good;
    },
 
    getItemSelector: function (cmp, el, ev) {
        var view = ST.fly(el).getComponent(),
            item = view.findItemByChild && view.findItemByChild(el),
            cell, col, colId, recId, ret, idx;
 
        if (item) {
            recId = item.getAttribute('data-recordindex');
            if (recId) {
                ret = [ item, '[data-recordindex=' + JSON.stringify(recId) + ']' ];
 
                for (cell = el; cell && cell !== item; cell = cell.parentNode) {
                    colId = cell.getAttribute('data-columnid');
                    col = colId && ST.Ext.getCmp(colId);
 
                    if (col) {
                        if (!col.autoGenId) {
                            ret[0] = cell;
                            ret[1] += ' [data-columnid=' + JSON.stringify(colId) + ']';
                        }
                        break;
                    }
                }
            } else {
                // try to find item by index...not perfect, but something 
                idx = el.tagName.toLowerCase() === 'li' && view.indexOf && view.indexOf(el);
 
                if (typeof idx !== 'undefined' && idx >= 0) {
                    ret = [ item, 'li:nth-child(' + (idx+1) + ')'];
                }
            }
        } else if (ev && this.includeFullLocator[ev.type]) {
            var matches = cmp.el.dom.querySelectorAll(el.tagName);
            if (matches && matches.length === 1) {
                ret = [ el, el.tagName.toLowerCase() ];
            }
        }
 
        return ret;
    },
 
    extractors: {
        iconCls: function (cmp, targets) {
            var iconCls = this.splitCls(cmp.iconCls);
 
            if (iconCls && iconCls.length === 1) {
                if (targets) {
                    targets.push([
                        cmp.el.dom,
                        this.getXType(cmp) + '[iconCls="' + iconCls[0] + '"]'
                    ]);
                }
 
                return 1;
            }
 
            return 0;
        },
 
        id: function (cmp, targets) {
            var me = this,
                id = me.getCmpId(cmp),
                parent;
 
            if (cmp.autoGenId || !me.validIdRe.test(id) || me.autoGenIdRe.test(id)) {
                return 0;
            }
 
            // We can still have an autoGenId on a parent that is then used to produce 
            // this id (panel-1010_header). So start with the parent and see if its id 
            // is a prefix of cmp's id and if it is an autoGenId. 
            for (parent = cmp; parent = parent.getRefOwner && parent.getRefOwner(); ) {
                if (!ST.String.startsWith(id, me.getCmpId(parent))) {
                    break;
                }
 
                if (parent.autoGenId) {
                    return 0;
                }
            }
 
            if (targets) {
                targets.push([
                    cmp.el.dom,
                    '#' + id
                ]);
            }
 
            return 1;
        }
    },
 
    /**
     * @property {Object} classIgnore 
     * Property names are the Ext JS classes to ignore.
     */
    classIgnore: {
        // 
    },
 
    /**
     * @property {Object} cmpIgnore 
     * Property names are the component xtypes to ignore. Values are either the xtype
     * of the parent if the component should only be ignored when inside this type of
     * parent, or `true` to always ignore.
     */
    cmpIgnore: {
        gridview: 'grid',
        tableview: 'grid',
        treeview: 'tree'
    },
 
    /**
     * @property {String[]} configList
     * The list of config properties used to identify components in order of priority.
     * @since 1.0.1
     */
    configList: [
        'id',
        'stateId',
        'reference',
        'itemId',
        'name',
        'iconCls',
        'text',
        'fieldLabel'
    ],
 
    /**
     * @property {Object} cssIgnore 
     * Property names are the CSS class names to ignore.
     */
    cssIgnore: {
        fa: 1,  // FontAwesome 
 
        // Some old framework bugs rendered null/undefined into class attribute 
        'null': 1,
        'undefined': 1
    },
 
    /**
     * Returns the id of the given Component.
     * @param {Ext.Component} cmp
     * @return {String}
     */
    getCmpId: function (cmp) {
        return cmp.getId ? cmp.getId() : cmp.id;
    },
 
    /**
     * Returns an array of `RegExp` patterns that describe CSS classes to be ignored.
     * @param {String} baseCSSPrefix The CSS prefix for Ext JS (typically "x-").
     * @return {String[]}
     * @since 1.0.1
     * @protected
     */
    getCssIgnorePatterns: function (baseCSSPrefix) {
        var x = baseCSSPrefix;
 
        if (!x) {
            return [];
        }
 
        x = '^' + x;
 
        return [
            x + 'noborder'
        ];
    },
 
    /**
     * Returns the `xtype` of the given Component. If the component has multiple xtypes,
     * the primary is returned.
     *
     * @param {Ext.Component} cmp
     * @return {String}
     * @since 1.0.1
     */
    getXType: function (cmp) {
        var xtype = cmp.getXType && cmp.getXType();
 
        if (!xtype) {
            xtype = cmp.xtype || (cmp.xtypes && cmp.xtypes[0]);
        }
 
        return xtype;
    },
 
    /**
     * Returns `true` if the given CSS class should be ignored.
     * @param {String} cls 
     * @return {Boolean}
     * @protected
     */
    ignoreCls: function (cls) {
        var cssIgnoreRe = this.cssIgnoreRe;
 
        return this.cssIgnore[cls] || (cssIgnoreRe && cssIgnoreRe.test(cls));
    },
 
    ignoreCmp: function (cmp) {
        var me = this,
            xtype = me.getXType(cmp),
            ignore = me.cmpIgnore[xtype],
            parent, parentXType;
 
        if (ignore) {
            if (typeof ignore !== 'string') {
                return true;
            }
 
            parent = cmp.getRefOwner();
            if (parent) {
                if (typeof parent.isXType === 'function') {
                    if (parent.isXType(ignore)) {
                        return true;
                    }
                } else {
                    parentXType = me.getXType(parent);
 
                    if (parentXType === ignore) {
                        return true;
                    }
                }
            }
        }
 
        if (me.classIgnore[cmp.$className]) {
            return true;
        }
 
        return false;
    },
 
    /**
     * Returns the array of CSS classes given the `className` (space-separated classes).
     * The ignored classes have been removed from the array.
     *
     * @param {String} cls 
     * @return {String[]}
     * @since 1.0.1
     * @protected
     */
    splitCls: function (cls) {
        var array = ST.String.split(ST.String.trim(cls)),
            len = array.length,
            c, i, ret;
 
        for (= 0; i < len; ++i) {
            c = array[i];
 
            if (&& !this.ignoreCls(c)) {
                (ret || (ret = [])).push(c);
            }
        }
 
        return ret;
    },
 
    customBuilders: {
        datePicker: function (cmp, el, targets, ev) {
            var me = this,
                node = el,
                classes = el.classList,
                len = targets.length,
                isItem = false,
                isField = !!cmp.pickerField, // could be standalone datepicker 
                isExt6 = ST.Ext.getVersion().gtEq(6),
                id, postLen, i, title, attr, tag;
                
            if (classes.contains('x-datepicker-cell')) {
                node = node;
                isItem = true;
            } else if (classes.contains('x-datepicker-date')) {
                node = node.parentNode;
                isItem = true;
            } else {
                el = cmp.el;
            }
            
            // if this is a picker attached to a field, make the field the root query object 
            if (isField) {
                cmp = cmp.pickerField
            }
 
            me.getCQ(cmp, el, targets, ev);
            postLen = targets.length;
 
            if (isItem) {
                id = node.id.replace(/^(datefield|datepicker)-[0-9]*-/, '');
                
                for (i=len; i<postLen; i++) {
                    if (isField) {
                        targets[i][1] += ' datepicker';
                    }
 
                    targets[i][0] = el;
 
                    // Ext JS 4/Ext JS 5 don't have a stable id for the cells, so we need to use something else 
                    // title isn't ideal, but it's there 
                    if (!isExt6) {
                        if (node.title) {
                            title = node.title;
                            attr = 'title';
                            tag = 'a';
                        } else {
                            title = node.getAttribute('aria-label');
                            attr = 'aria-label';
                            tag = 'div';
                        }
 
                        targets[i][1] += ' => ' + '[' + attr + '="' + title + '"] ' + tag;
                    } else {
                        targets[i][1] += ' => ' + '[id*="' + id + '"] div';
                    }                    
                }
            } else if (isField) {
                for (i=len; i<postLen; i++) {
                    targets[i][0] = el;
                    targets[i][1] += ' datepicker';
                }
            }    
 
            me.promoteLocators(targets, len, postLen-len);
        },
 
        timePicker: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                postLen, i;
 
            me.getCQ(cmp.pickerField, el, targets, ev);
            postLen = targets.length;
 
            for (i=len; i<postLen; i++) {
                targets[i][0] = el;
                targets[i][1] = targets[i][1].replace('=>', 'timepicker =>');
            }
 
            me.promoteLocators(targets, len, postLen-len);
        },
 
        boundList: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                postLen, i;
 
            me.getCQ(cmp.pickerField, el, targets, ev);
            postLen = targets.length;
 
            for (i=len; i<postLen; i++) {
                targets[i][0] = el;
                targets[i][1] = targets[i][1].replace('=>', 'boundlist =>');
            }
 
            me.promoteLocators(targets, len, postLen-len);
        },
 
        gridColumn: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                triggerCls = 'x-column-header-trigger',
                isTrigger = el.classList.contains(triggerCls),
                postLen, i, selector, type;
 
            me.getCQ(cmp.up('tablepanel'), el, targets, ev);
            postLen = targets.length;
 
            for (i=len; i<postLen; i++) {
                targets[i][0] = el;
                selector = cmp.dataIndex || cmp.getDataIndex && cmp.getDataIndex();
                type = 'dataIndex';
 
                if (!selector) {
                    selector = cmp.text || cmp.getText && cmp.getText();
                    type = 'text';
                } 
 
                if (selector) {
                    targets[i][1] += ' ' + me.getXType(cmp) + '[' + type + '="' + selector + '"]';
                
                    if (isTrigger) {
                        targets[i][1] += ' => .' + triggerCls;
                    }
                }
            }
 
            me.promoteLocators(targets, len, postLen-len);
        },
        
        menuItem: function (cmp, el, targets, ev) {
            var me = this,
                len = targets.length,
                buttonOwner = cmp.up('button'),
                headerOwner = cmp.up('headercontainer'),
                prereqs = [],
                postLen, i, fn;
            
            if (buttonOwner) {
                me.getCQ(buttonOwner, el, targets, ev);
                postLen = targets.length;
 
                for (i=len; i<postLen; i++) {
                    targets[i][0] = el;
                    targets[i][1] += ' menu > ' + me.getXType(cmp) + '[text="' + cmp.text + '"]';
                }
            } else if (headerOwner) {
                me.getCQ(cmp.up('grid'), el, targets, ev);
                postLen = targets.length;
 
                for (i=len; i<postLen; i++) {
                    targets[i][0] = el;
                    targets[i][1] += ' menu > ' + me.getXType(cmp) + '[text="' + cmp.text + '"]';
                }
            }
 
            if (ev && (ev.type === 'mousedown' || ev.type === 'pointerdown')) {
                fn = function (_cmp) {
                    var itemOwner = _cmp.up('menuitem'), 
                        ownerTargets = [],
                        prereq, x;
 
                    if (itemOwner) {
                        me.locate(itemOwner.el, ownerTargets, ev);
                        prereq = {
                            type: 'menuitemover',
                            target: ownerTargets[0][1],
                            targets: []
                        };
 
                        for (x=0; x<ownerTargets.length; x++) {
                            prereq.targets.push({
                                target: ownerTargets[x][1]
                            });
                        }
                        
                        prereqs.push(prereq);
 
                        // call recursively until we reach the top 
                        fn(itemOwner);
                    }
                }
                
                fn(cmp);
 
                if (prereqs.length) {
                    ev.prereq = prereqs.reverse();
                }
            }
 
            me.promoteLocators(targets, len, postLen-len);
        }
    },
 
    promoteLocators: function (targets, pos, count) {
        var spliced = targets.splice(pos, count).reverse(),
            i;
        
        for (i=0; i<spliced.length; i++) {
            targets.unshift(spliced[i]); 
        }        
    },
 
    findCustomBuilder: function (cmp) {
        var chain = cmp.xtypesChain.slice(), // copy so we don't mutate the original!!! 
            xtypeList = chain.reverse(), // reverse the xytype list so we get most specific first 
            matchers = ['datepicker', 'timepicker', 'boundlist', 'gridcolumn', 'menuitem'],
            len = xtypeList.length,
            xtype, i, idx;
 
        for (i=0; i<len; i++) {
            idx = Ext.Array.indexOf(matchers, xtypeList[i]);
            if (idx !== -1) {
                xtype = matchers[idx];
                break;
            }
        }
        return xtype;
    },
 
    processCustomBuilder: function (cmp, el, targets, ev) {
        var me = this,
            xtype = me.findCustomBuilder(cmp);
        
        if (xtype && ST.isClassic) {
            switch (xtype) {
                case 'datepicker':
                    me.customBuilders.datePicker.apply(me, arguments);
                    break;
                case 'timepicker':
                    me.customBuilders.timePicker.apply(me, arguments);
                    break;
                case 'boundlist':
                    me.customBuilders.boundList.apply(me, arguments);
                    break;
                case 'gridcolumn':
                    me.customBuilders.gridColumn.apply(me, arguments);
                    break;
                case 'menuitem':
                    me.customBuilders.menuItem.apply(me, arguments);
                    break;
            }
        }
    }
}, function (Strategy) {
    function make (prop, includeXType, skipGetter) {
        var attr = '[' + prop + '=',
            getter = 'get' + prop.charAt(0).toUpperCase() + prop.substring(1);
 
        Strategy.prototype.extractors[prop] = function (cmp, targets) {
            var value;
 
            if (skipGetter || prop in cmp) {
                value = cmp[prop];
            }
            else if (cmp[getter]) {
                value = cmp[getter]();
            }
 
            if (value || value === 0) {
                if (targets) {
                    // some configs (like text) could have quotes 
                    targets.push([
                        cmp.el.dom,
                        (includeXType ? this.getXType(cmp) : '') +
                            attr + JSON.stringify(value) + ']'
                    ]);
                }
 
                return true;
            }
        };
    }
 
    make('reference', true);
    make('itemId', true, true);
    make('name', true, true);
    make('stateId');
    make('text', true);
    make('fieldLabel', true);
});