// @tag class
/**
 * @class Ext.Inventory
 * @private
 */
Ext.Inventory = function() {
// @define Ext.Script
// @define Ext.Inventory
// @require Ext.Function
    var me = this;
 
    me.names = [];
    me.paths = {};
    
    me.alternateToName = {};
    me.aliasToName = {};
    me.nameToAliases = {};
    me.nameToAlternates = {};
    me.nameToPrefix = {};
};
 
Ext.Inventory.prototype = {
    _array1: [0],
 
    prefixes: null,
 
    dotRe: /\./g,
    wildcardRe: /\*/g,
 
    addAlias: function(className, alias, update) {
        return this.addMapping(className, alias, this.aliasToName, this.nameToAliases, update);
    },
 
    addAlternate: function(className, alternate) {
        return this.addMapping(className, alternate, this.alternateToName, this.nameToAlternates);
    },
 
    addMapping: function(className, alternate, toName, nameTo, update) {
        var name = className.$className || className,
            mappings = name,
            array = this._array1,
            a, aliases, cls, i, length,
            nameMapping;
 
        if (Ext.isString(name)) {
            mappings = {};
            mappings[name] = alternate;
        }
 
        for (cls in mappings) {
            aliases = mappings[cls];
            
            if (Ext.isString(aliases)) {
                array[0] = aliases;
                aliases = array;
            }
 
            length = aliases.length;
            nameMapping = nameTo[cls] || (nameTo[cls] = []);
            
            for (= 0; i < length; ++i) {
                if (!(= aliases[i])) {
                    continue;
                }
 
                if (toName[a] !== cls) {
                    //<debug>
                    if (!update && toName[a] && ('Ext.Gadget' !== a)) {
                        Ext.log.warn("Overriding existing mapping: '" + a + "' From '" +
                            toName[a] + "' to '" + cls + "'. Is this intentional?");
                    }
                    //</debug>
 
                    toName[a] = cls;
                    nameMapping.push(a);
                }
            }
        }
    },
 
    /**
     * Get the aliases of a class by the class name
     *
     * @param {String} name 
     * @return {Array} aliases
     */
    getAliasesByName: function(name) {
        return this.nameToAliases[name] || null;
    },
 
    getAlternatesByName: function(name) {
        return this.nameToAlternates[name] || null;
    },
 
    /**
     * Get the name of a class by its alias.
     *
     * @param {String} alias 
     * @return {String} className
     */
    getNameByAlias: function(alias) {
        return this.aliasToName[alias] || '';
    },
 
    /**
     * Get the name of a class by its alternate name.
     *
     * @param {String} alternate 
     * @return {String} className
     */
    getNameByAlternate: function(alternate) {
        return this.alternateToName[alternate] || '';
    },
 
    /**
     * Converts a string expression to an array of matching class names. An expression can
     * either refers to class aliases or class names. Expressions support wildcards:
     *
     *      // returns ['Ext.window.Window']
     *     var window = Ext.ClassManager.getNamesByExpression('widget.window');
     *
     *     // returns ['widget.panel', 'widget.window', ...]
     *     var allWidgets = Ext.ClassManager.getNamesByExpression('widget.*');
     *
     *     // returns ['Ext.data.Store', 'Ext.data.ArrayProxy', ...]
     *     var allData = Ext.ClassManager.getNamesByExpression('Ext.data.*');
     *
     * @param {String/String[]} expression
     * @param {Object} [exclude=null] An object keyed by class name containing classes to
     * exclude from the returned classes. This must be provided if `accumulate` is set to
     * `true`.
     * @param {Boolean} [accumulate=false] Pass `true` to add matching classes to the
     * specified `exclude` object.
     * @return {String[]} An array of class names.
     */
    getNamesByExpression: function(expression, exclude, accumulate) {
        var me = this,
            aliasToName = me.aliasToName,
            alternateToName = me.alternateToName,
            nameToAliases = me.nameToAliases,
            nameToAlternates = me.nameToAlternates,
            map = accumulate ? exclude : {},
            names = [],
            expressions = Ext.isString(expression) ? [expression] : expression,
            length = expressions.length,
            wildcardRe = me.wildcardRe,
            expr, i, list, match, n, name, regex;
 
        for (= 0; i < length; ++i) {
            if ((expr = expressions[i]).indexOf('*') < 0) {
                // No wildcard
                if (!(name = aliasToName[expr])) {
                    if (!(name = alternateToName[expr])) {
                        name = expr;
                    }
                }
 
                if (!(name in map) && !(exclude && (name in exclude))) {
                    map[name] = 1;
                    names.push(name);
                }
            }
            else {
                regex = new RegExp('^' + expr.replace(wildcardRe, '(.*?)') + '$');
 
                for (name in nameToAliases) {
                    if (!(name in map) && !(exclude && (name in exclude))) {
                        if (!(match = regex.test(name))) {
                            n = (list = nameToAliases[name]).length;
                            
                            while (!match && n-- > 0) {
                                match = regex.test(list[n]);
                            }
 
                            list = nameToAlternates[name];
                            
                            if (list && !match) {
                                n = list.length;
                                
                                while (!match && n-- > 0) {
                                    match = regex.test(list[n]);
                                }
                            }
                        }
 
                        if (match) {
                            map[name] = 1;
                            names.push(name);
                        }
                    }
                }
            }
        }
 
        return names;
    },
 
    getPath: function(className) {
        var me = this,
            paths = me.paths,
            ret = '',
            prefix;
 
        if (className in paths) {
            ret = paths[className];
        }
        else {
            prefix = me.nameToPrefix[className] ||
                     (me.nameToPrefix[className] = me.getPrefix(className));
            
            if (prefix) {
                className = className.substring(prefix.length + 1);
                ret = paths[prefix];
                
                if (ret) {
                    ret += '/';
                }
            }
 
            ret += className.replace(me.dotRe, '/') + '.js';
        }
 
        return ret;
    },
 
    getPrefix: function(className) {
        if (className in this.paths) {
            return className;
        }
        else if (className in this.nameToPrefix) {
            return this.nameToPrefix[className];
        }
 
        /* eslint-disable-next-line vars-on-top */
        var prefixes = this.getPrefixes(),
            length = className.length,
            items, currChar, prefix, j, jlen;
        
        // Walk the prefixes backwards so we consider the longest ones first.
        // Prefixes are kept in a sparse array grouped by length so we don't have to
        // iterate over all of them, just the ones we need.
        while (length-- > 0) {
            items = prefixes[length];
            
            if (items) {
                currChar = className.charAt(length);
                
                if (currChar !== '.') {
                    continue;
                }
                
                for (= 0, jlen = items.length; j < jlen; j++) {
                    prefix = items[j];
                    
                    if (prefix === className.substring(0, length)) {
                        return prefix;
                    }
                }
            }
        }
 
        return '';
    },
 
    getPrefixes: function() {
        var me = this,
            prefixes = me.prefixes,
            names, name, nameLength, items, i, len;
 
        if (!prefixes) {
            names = me.names.slice(0);
            me.prefixes = prefixes = [];
            
            for (= 0, len = names.length; i < len; i++) {
                name = names[i];
                nameLength = name.length;
                
                items = prefixes[nameLength] || (prefixes[nameLength] = []);
                
                items.push(name);
            }
        }
 
        return prefixes;
    },
 
    removeName: function(name) {
        var me = this,
            aliasToName = me.aliasToName,
            alternateToName = me.alternateToName,
            nameToAliases = me.nameToAliases,
            nameToAlternates = me.nameToAlternates,
            aliases = nameToAliases[name],
            alternates = nameToAlternates[name],
            i, a;
 
        delete nameToAliases[name];
        delete nameToAlternates[name];
        delete me.nameToPrefix[name];
 
        if (aliases) {
            for (= aliases.length; i--;) {
                // Aliases can be reassigned so if this class is the current mapping of
                // the alias, remove it. Since there is no chain to restore what was
                // removed this is not perfect.
                if (name === aliasToName[= aliases[i]]) {
                    delete aliasToName[a];
                }
            }
        }
 
        if (alternates) {
            for (= alternates.length; i--;) {
                // Like aliases, alternate class names can also be remapped.
                if (name === alternateToName[= alternates[i]]) {
                    delete alternateToName[a];
                }
            }
        }
    },
 
    resolveName: function(name) {
        var me = this,
            trueName;
 
        // If the name has a registered alias, it is a true className (not an alternate)
        // so we can stop now.
        if (!(name in me.nameToAliases)) {
            // The name is not a known class name, so check to see if it is a known alias:
            if (!(trueName = me.aliasToName[name])) {
                // The name does not correspond to a known alias, so check if it is a known
                // alternateClassName:
                trueName = me.alternateToName[name];
            }
        }
 
        return trueName || name;
    },
 
    /**
     * This method returns a selector object that produces a selection of classes and
     * delivers them to the desired `receiver`.
     * 
     * The returned selector object has the same methods as the given `receiver` object
     * but these methods on the selector accept a first argument that expects a pattern
     * or array of patterns. The actual method on the `receiver` will be called with an
     * array of classes that match these patterns but with any patterns passed to an
     * `exclude` call removed.
     * 
     * For example:
     * 
     *      var sel = inventory.select({
     *              require: function (classes) {
     *                  console.log('Classes: ' + classes.join(','));
     *              }
     *          });
     * 
     *      sel.exclude('Ext.chart.*').exclude('Ext.draw.*').require('*');
     *      
     *      // Logs all classes except those in the Ext.chart and Ext.draw namespaces.
     * 
     * @param {Object} receiver 
     * @param {Object} [scope] Optional scope to use when calling `receiver` methods.
     * @return {Object} An object with the same methods as `receiver` plus `exclude`.
     */
    select: function(receiver, scope) {
        var me = this,
            excludes = {},
            ret = {
                excludes: excludes,
 
                exclude: function() {
                    me.getNamesByExpression(arguments[0], excludes, true);
                    
                    return this;
                }
            },
            name;
 
        for (name in receiver) {
            ret[name] = me.selectMethod(excludes, receiver[name], scope || receiver);
        }
 
        return ret;
    },
 
    selectMethod: function(excludes, fn, scope) {
        var me = this;
 
        return function(include) {
            var args = Ext.Array.slice(arguments, 1);
            
            args.unshift(me.getNamesByExpression(include, excludes));
 
            return fn.apply(scope, args);
        };
    },
 
    /**
     * Sets the path of a namespace.
     * For Example:
     *
     *      inventory.setPath('Ext', '.');
     *      inventory.setPath({
     *          Ext: '.'
     *      });
     *
     * @param {String/Object} name The name of a single mapping or an object of mappings.
     * @param {String} [path] If `name` is a String, then this is the path for that name.
     * Otherwise this parameter is ignored.
     * @return {Ext.Inventory} this
     * @method
     */
    setPath: Ext.Function.flexSetter(function(name, path) {
        var me = this;
 
        me.paths[name] = path;
        me.names.push(name);
 
        me.prefixes = null;
        me.nameToPrefix = {};
 
        return me;
    })
};