// @tag class
/**
 * This class provides dynamic loading support for JavaScript classes. Application code
 * does not typically need to call `Ext.Loader` except perhaps to configure path mappings
 * when not using [Sencha Cmd](http://www.sencha.com/products/sencha-cmd/).
 *
 *      Ext.Loader.setPath('MyApp', 'app');
 *
 * When using Sencha Cmd, this is handled by the "bootstrap" provided by the application
 * build script and such configuration is not necessary.
 *
 * # Typical Usage
 *
 * The `Ext.Loader` is most often used behind the scenes to satisfy class references in
 * class declarations. Like so:
 *
 *      Ext.define('MyApp.view.Main', {
 *          extend: 'Ext.panel.Panel',
 *
 *          mixins: [
 *              'MyApp.util.Mixin'
 *          ],
 *
 *          requires: [
 *              'Ext.grid.Panel'
 *          ],
 *
 *          uses: [
 *              'MyApp.util.Stuff'
 *          ]
 *      });
 *
 * In all of these cases, `Ext.Loader` is used internally to resolve these class names
 * and ensure that the necessary class files are loaded.
 *
 * During development, these files are loaded individually for optimal debugging. For a
 * production use, [Sencha Cmd](http://www.sencha.com/products/sencha-cmd/) will replace
 * all of these strings with the actual resolved class references because it ensures that
 * the classes are all contained in the build in the correct order. In development, these
 * files will not be loaded until the `MyApp.view.Main` class indicates they are needed
 * as shown above.
 *
 * # Loading Classes
 *
 * You can also use `Ext.Loader` directly to load classes or files. The simplest form of
 * use is `{@link Ext#require}`.
 *
 * For example:
 *
 *      Ext.require('MyApp.view.Main', function () {
 *          // On callback, the MyApp.view.Main class is now loaded
 *
 *          var view = new MyApp.view.Main();
 *      });
 *
 * You can alternatively require classes by alias or wildcard.
 *
 *     Ext.require('widget.window');
 *
 *     Ext.require(['widget.window', 'layout.border', 'Ext.data.Connection']);
 *
 *     Ext.require(['widget.*', 'layout.*', 'Ext.data.*']);
 *
 * The callback function is optional.
 *
 * **Note** Using `Ext.require` at global scope will cause `{@link Ext#onReady}` and
 * `{@link Ext.app.Application#launch}` methods to be deferred until the required classes
 * are loaded. It is these cases where the callback function is most often unnecessary.
 *
 * ## Using Excludes
 *
 * Alternatively, you can exclude what you don't need:
 *
 *     // Include everything except Ext.tree.*
 *     Ext.exclude('Ext.tree.*').require('*');
 *
 *     // Include all widgets except widget.checkbox* (this will exclude
 *     // widget.checkbox, widget.checkboxfield, widget.checkboxgroup, etc.)
 *     Ext.exclude('widget.checkbox*').require('widget.*');
 *
 * # Dynamic Instantiation
 *
 * Another feature enabled by `Ext.Loader` is instantiation using class names or aliases.
 *
 * For example:
 *
 *      var win = Ext.create({
 *          xtype: 'window',
 *
 *          // or
 *          // xclass: 'Ext.window.Window'
 *
 *          title: 'Hello'
 *      });
 *
 * This form of creation can be useful if the type to create (`window` in the above) is
 * not known statically. Internally, `{@link Ext#method!create}` may need to *synchronously*
 * load the desired class and its requirements. Doing this will generate a warning in
 * the console:
 * 
 *      [Ext.Loader] Synchronously loading 'Ext.window.Window'...
 *
 * If you see these in your debug console, you should add the indicated class(es) to the
 * appropriate `requires` array (as above) or make an `{@link Ext#require}` call.
 * 
 * 
 * **Note** Using `{@link Ext#method!create}` has some performance overhead and is best reserved
 * for cases where the target class is not known until run-time.
 * 
 * @class Ext.Loader
 * @singleton
 */
Ext.Loader = (new function() {
// @define Ext.Loader
// @require Ext.Base
// @require Ext.Class
// @require Ext.ClassManager
// @require Ext.mixin.Watchable
// @require Ext.Function
// @require Ext.Array
// @require Ext.env.Ready
 
    var Loader = this,
        Manager = Ext.ClassManager, // this is an instance of Ext.Inventory
        Boot = Ext.Boot,
        Class = Ext.Class,
        Ready = Ext.env.Ready,
        alias = Ext.Function.alias,
        dependencyProperties = ['extend', 'mixins', 'requires'],
        isInHistory = {},
        history = [],
        readyListeners = [],
        usedClasses = [],
        _requiresMap = {},
        _config = {
            /**
             * @cfg {Boolean} [enabled=true]
             * Whether or not to enable the dynamic dependency loading feature.
             */
            enabled: true,
 
            /**
             * @cfg {Boolean} [scriptChainDelay=false]
             * millisecond delay between asynchronous script injection (prevents stack
             * overflow on some user agents) 'false' disables delay but potentially
             * increases stack load.
             */
            scriptChainDelay: false,
 
            /**
             * @cfg {Boolean} [disableCaching=true]
             * Appends current timestamp to script files to prevent caching.
             */
            disableCaching: true,
 
            /**
             * @cfg {String} [disableCachingParam="_dc"]
             * The get parameter name for the cache buster's timestamp.
             */
            disableCachingParam: '_dc',
 
            /**
             * @cfg {Object} paths
             * The mapping from namespaces to file paths
             *
             *     {
             *         'Ext': '.', // This is set by default, Ext.layout.container.Container will be
             *                     // loaded from ./layout/Container.js
             *
             *         'My': './src/my_own_folder' // My.layout.Container will be loaded from
             *                                     // ./src/my_own_folder/layout/Container.js
             *     }
             *
             * Note that all relative paths are relative to the current HTML document.
             * If not being specified, for example, `Other.awesome.Class` will simply be
             * loaded from `"./Other/awesome/Class.js"`.
             */
            paths: Manager.paths,
 
            /**
             * @cfg {Boolean} preserveScripts
             * `false` to remove asynchronously loaded scripts, `true` to retain script
             * element for browser debugger compatibility and improved load performance.
             */
            preserveScripts: true,
 
            /**
             * @cfg {String} scriptCharset
             * Optional charset to specify encoding of dynamic script content.
             */
            scriptCharset: undefined
        },
        // These configs are delegated to Ext.Script and may need different names:
        delegatedConfigs = {
            disableCaching: true,
            disableCachingParam: true,
            preserveScripts: true,
            scriptChainDelay: 'loadDelay'
        };
 
    Ext.apply(Loader, {
        /**
         * @private
         */
        isInHistory: isInHistory,
 
        /**
         * Flag indicating whether there are still files being loaded
         * @private
         */
        isLoading: false,
 
        /**
         * An array of class names to keep track of the dependency loading order.
         * This is not guaranteed to be the same everytime due to the asynchronous
         * nature of the Loader.
         *
         * @property {Array} history
         */
        history: history,
 
        /**
         * Configuration
         * @private
         */
        config: _config,
 
        /**
         * Maintain the list of listeners to execute when all required scripts are fully loaded
         * @private
         */
        readyListeners: readyListeners,
 
        /**
         * Contains classes referenced in `uses` properties.
         * @private
         */
        optionalRequires: usedClasses,
 
        /**
         * Map of fully qualified class names to an array of dependent classes.
         * @private
         */
        requiresMap: _requiresMap,
 
        /** @private */
        hasFileLoadError: false,
 
        /**
         * The number of scripts loading via loadScript.
         * @private
         */
        scriptsLoading: 0,
 
        /**
         * @private
         */
        classesLoading: {},
        missingCount: 0,
        missingQueue: {},
        
        /**
         * @private
         */
        syncModeEnabled: false,
        
        init: function() {
            // initalize the default path of the framework
            var scripts = document.getElementsByTagName('script'),
                src = scripts[scripts.length - 1].src,
                path = src.substring(0, src.lastIndexOf('/') + 1),
                meta = Ext._classPathMetadata,
                microloader = Ext.Microloader,
                manifest = Ext.manifest,
                loadOrder, baseUrl, loadlen, l, loadItem;
 
            //<debug>
            if (src.indexOf("packages/core/src/") !== -1) {
                path = path + "../../";
            }
            else if (src.indexOf("/core/src/class/") !== -1) {
                path = path + "../../../";
            }
            //</debug>
 
            
            if (!Manager.getPath("Ext")) {
                Manager.setPath('Ext', path + 'src');
            }
 
            // Pull in Cmd generated metadata if available.
            if (meta) {
                Ext._classPathMetadata = null;
                Loader.addClassPathMappings(meta);
            }
            
            if (manifest) {
                loadOrder = manifest.loadOrder;
                
                // if the manifest paths were calculated as relative to the 
                // bootstrap file, then we need to prepend Boot.baseUrl to the
                // paths before processing
                baseUrl = Ext.Boot.baseUrl;
                
                if (loadOrder && manifest.bootRelative) {
                    for (loadlen = loadOrder.length, l = 0; l < loadlen; l++) {
                        loadItem = loadOrder[l];
                        loadItem.path = baseUrl + loadItem.path;
                        loadItem.canonicalPath = true;
                    }
                }
            }
            
            if (microloader) {
                Ready.block();
                
                microloader.onMicroloaderReady(function() {
                    Ready.unblock();
                });
            }
        },
 
        /**
         * @method setConfig
         * Set the configuration for the loader. This should be called right after ext-(debug).js
         * is included in the page, and before Ext.onReady. i.e:
         *
         *     <script type="text/javascript" src="ext-core-debug.js"></script>
         *     <script type="text/javascript">
         *         Ext.Loader.setConfig({
         *           enabled: true,
         *           paths: {
         *               'My': 'my_own_path'
         *           }
         *         });
         *     </script>
         *     <script type="text/javascript">
         *         Ext.require(...);
         *
         *         Ext.onReady(function() {
         *           // application code here
         *         });
         *     </script>
         *
         * Refer to config options of {@link Ext.Loader} for the list of possible properties
         *
         * @param {Object} config The config object to override the default values
         * @return {Ext.Loader} this
         */
        setConfig: Ext.Function.flexSetter(function(name, value) {
            var delegated = delegatedConfigs[name];
 
            if (name === 'paths') {
                Loader.setPath(value);
            }
            else {
                _config[name] = value;
 
                if (delegated) {
                    Boot.setConfig((delegated === true) ? name : delegated, value);
                }
            }
 
            return Loader;
        }),
 
        /**
         * Get the config value corresponding to the specified name. If no name is given,
         * will return the config object
         *
         * @param {String} name The config property name
         * @return {Object} 
         */
        getConfig: function(name) {
            return name ? _config[name] : _config;
        },
 
        /**
         * Sets the path of a namespace.
         * For Example:
         *
         *     Ext.Loader.setPath('Ext', '.');
         *
         * @param {String/Object} name See {@link Ext.Function#flexSetter flexSetter}
         * @param {String} [path] See {@link Ext.Function#flexSetter flexSetter}
         * @return {Ext.Loader} this
         * @method
         */
        setPath: function() {
            // Paths are an Ext.Inventory thing and ClassManager is an instance of that:
            Manager.setPath.apply(Manager, arguments);
            
            return Loader;
        },
 
        /**
         * Sets a batch of path entries
         *
         * @param {Object} paths a set of className: path mappings
         * @return {Ext.Loader} this
         */
        addClassPathMappings: function(paths) {
            // Paths are an Ext.Inventory thing and ClassManager is an instance of that:
            Manager.setPath(paths);
            
            return Loader;
        },
 
        /**
         * fixes up loader path configs by prepending Ext.Boot#baseUrl to the beginning
         * of the path, then delegates to Ext.Loader#addClassPathMappings
         * @param pathConfig
         */
        addBaseUrlClassPathMappings: function(pathConfig) {
            var name;
            
            for (name in pathConfig) {
                pathConfig[name] = Boot.baseUrl + pathConfig[name];
            }
            
            Ext.Loader.addClassPathMappings(pathConfig);
        },
 
        /**
         * Translates a className to a file path by adding the
         * the proper prefix and converting the .'s to /'s. For example:
         *
         *     Ext.Loader.setPath('My', '/path/to/My');
         *
         *     // alerts '/path/to/My/awesome/Class.js'
         *     alert(Ext.Loader.getPath('My.awesome.Class'));
         *
         * Note that the deeper namespace levels, if explicitly set, are always resolved first.
         * For example:
         *
         *     Ext.Loader.setPath({
         *         'My': '/path/to/lib',
         *         'My.awesome': '/other/path/for/awesome/stuff',
         *         'My.awesome.more': '/more/awesome/path'
         *     });
         *
         *     // alerts '/other/path/for/awesome/stuff/Class.js'
         *     alert(Ext.Loader.getPath('My.awesome.Class'));
         *
         *     // alerts '/more/awesome/path/Class.js'
         *     alert(Ext.Loader.getPath('My.awesome.more.Class'));
         *
         *     // alerts '/path/to/lib/cool/Class.js'
         *     alert(Ext.Loader.getPath('My.cool.Class'));
         *
         *     // alerts 'Unknown/strange/Stuff.js'
         *     alert(Ext.Loader.getPath('Unknown.strange.Stuff'));
         *
         * @param {String} className 
         * @return {String} path
         */
        getPath: function(className) {
            // Paths are an Ext.Inventory thing and ClassManager is an instance of that:
            return Manager.getPath(className);
        },
 
        require: function(expressions, fn, scope, excludes) {
            var classNames;
            
            if (excludes) {
                return Loader.exclude(excludes).require(expressions, fn, scope);
            }
 
            classNames = Manager.getNamesByExpression(expressions);
 
            return Loader.load(classNames, fn, scope);
        },
 
        syncRequire: function() {
            var wasEnabled = Loader.syncModeEnabled,
                ret;
 
            Loader.syncModeEnabled = true;
            ret = Loader.require.apply(Loader, arguments);
            Loader.syncModeEnabled = wasEnabled;
 
            return ret;
        },
 
        exclude: function(excludes) {
            var selector = Manager.select({
                require: function(classNames, fn, scope) {
                    return Loader.load(classNames, fn, scope);
                },
 
                syncRequire: function(classNames, fn, scope) {
                    var wasEnabled = Loader.syncModeEnabled,
                        ret;
 
                    Loader.syncModeEnabled = true;
                    ret = Loader.load(classNames, fn, scope);
                    Loader.syncModeEnabled = wasEnabled;
 
                    return ret;
                }
            });
 
            selector.exclude(excludes);
            
            return selector;
        },
 
        load: function(classNames, callback, scope) {
            if (callback) {
                if (callback.length) {
                    // If callback expects arguments, shim it with a function that will map
                    // the requires class(es) from the names we are given.
                    callback = Loader.makeLoadCallback(classNames, callback);
                }
                
                callback = callback.bind(scope || Ext.global);
            }
 
            /* eslint-disable-next-line vars-on-top */
            var state = Manager.classState,
                missingClassNames = [],
                urls = [],
                urlByClass = {},
                numClasses = classNames.length,
                className, i, numMissing;
            
            for (= 0; i < numClasses; ++i) {
                className = Manager.resolveName(classNames[i]);
                
                if (!Manager.isCreated(className)) {
                    missingClassNames.push(className);
                    
                    if (!state[className]) {
                        urlByClass[className] = Loader.getPath(className);
                        urls.push(urlByClass[className]);
                    }
                }
            }
            
            // If the dynamic dependency feature is not being used, throw an error
            // if the dependencies are not defined
            numMissing = missingClassNames.length;
            
            if (numMissing) {
                Loader.missingCount += numMissing;
                
                Manager.onCreated(function() {
                    if (callback) {
                        Ext.callback(callback, scope, arguments);
                    }
                    
                    Loader.checkReady();
                }, Loader, missingClassNames);
 
                if (!_config.enabled) {
                    Ext.raise("Ext.Loader is not enabled, so dependencies cannot be resolved " +
                              "dynamically. Missing required class" +
                              ((missingClassNames.length > 1) ? "es" : "") + "" +
                              missingClassNames.join(''));
                }
 
                if (urls.length) {
                    Loader.loadScripts({
                        url: urls,
                        // scope will be this options object so we can pass these along:
                        _classNames: missingClassNames,
                        _urlByClass: urlByClass
                    });
                }
                else {
                    // need to call checkReady here, as the _missingCoun
                    // may have transitioned from 0 to > 0, meaning we
                    // need to block ready
                    Loader.checkReady();
                }
            }
            else {
                if (callback) {
                    callback.call(scope);
                }
                
                // need to call checkReady here, as the _missingCoun
                // may have transitioned from 0 to > 0, meaning we
                // need to block ready
                Loader.checkReady();
            }
            
            if (Loader.syncModeEnabled) {
                // Class may have been just loaded or was already loaded
                if (numClasses === 1) {
                    return Manager.get(classNames[0]);
                }
            }
 
            return Loader;
        },
 
        makeLoadCallback: function(classNames, callback) {
            return function() {
                var classes = [],
                    i = classNames.length;
 
                while (i-- > 0) {
                    classes[i] = Manager.get(classNames[i]);
                }
 
                return callback.apply(this, classes);
            };
        },
        
        onLoadFailure: function(request) {
            var options = this,
                entries = request.entries || [],
                onError = options.onError,
                error, entry, i;
 
            Loader.hasFileLoadError = true;
            --Loader.scriptsLoading;
 
            if (onError) {
                for (= 0; i < entries.length; i++) {
                    entry = entries[i];
                    
                    if (entry.error) {
                        error = new Error('Failed to load: ' + entry.url);
                        break;
                    }
                }
                
                error = error || new Error('Failed to load');
                onError.call(options.userScope, options, error, request);
            }
            //<debug>
            else {
                Ext.log.error("[Ext.Loader] Some requested files failed to load.");
            }
            //</debug>
 
            Loader.checkReady();
        },
 
        onLoadSuccess: function() {
            var options = this,
                onLoad = options.onLoad,
                classNames = options._classNames,
                urlByClass = options._urlByClass,
                state = Manager.classState,
                missingQueue = Loader.missingQueue,
                className, i, len;
            
            --Loader.scriptsLoading;
            
            if (onLoad) {
                // TODO: need an adapter to convert to v4 onLoad signatures
                onLoad.call(options.userScope, options);
                // onLoad can cause more loads to start, so it must run first
            }
            
            // classNames is the array of *all* classes that load() was asked to load,
            // including those that might have been already loaded but not yet created.
            // urlByClass is a map of only those classes that we asked Boot to load.
            for (= 0, len = classNames.length; i < len; i++) {
                className = classNames[i];
                
                // When a script is loaded and executed, we should have Ext.define() called
                // for at least one of the classes in the list, which will set the state
                // for that class. That by itself does not mean that the class is available
                // *now* but it means that ClassManager is tracking it and will fire the
                // onCreated callback that we set back in load().
                // However if there is no state for the class, that may mean two things:
                // either it is not a Ext class, or it is truly missing. In any case we need
                // to watch for that thing ourselves, which we will do every checkReady().
                if (!state[className]) {
                    missingQueue[className] = urlByClass[className];
                }
            }
            
            Loader.checkReady();
        },
 
        // TODO: this timing of this needs to be deferred until all classes have had
        // a chance to be created
        //<debug>
        reportMissingClasses: function() {
            var missingQueue = Loader.missingQueue,
                missingClasses = [],
                missingPaths = [],
                missingClassName;
 
            if (!Loader.syncModeEnabled && !Loader.scriptsLoading && Loader.isLoading &&
                    !Loader.hasFileLoadError) {
                for (missingClassName in missingQueue) {
                    missingClasses.push(missingClassName);
                    missingPaths.push(missingQueue[missingClassName]);
                }
 
                if (missingClasses.length) {
                    throw new Error("The following classes are not declared even if their files " +
                                    "have been loaded: '" + missingClasses.join("', '") +
                                    "'. Please check the source code of their " +
                                    "corresponding files for possible typos: '" +
                                    missingPaths.join("', '"));
                }
            }
        },
        //</debug>
 
        /**
         * Add a new listener to be executed when all required scripts are fully loaded
         *
         * @param {Function} fn The function callback to be executed
         * @param {Object} scope The execution scope (`this`) of the callback function.
         * @param {Boolean} [withDomReady=true] Pass `false` to not also wait for document
         * dom ready.
         * @param {Object} [options] Additional callback options.
         * @param {Number} [options.delay=0] A number of milliseconds to delay.
         * @param {Number} [options.priority=0] Relative priority of this callback. Negative
         * numbers are reserved.
         */
        onReady: function(fn, scope, withDomReady, options) {
            var listener;
            
            if (withDomReady) {
                Ready.on(fn, scope, options);
            }
            else {
                listener = Ready.makeListener(fn, scope, options);
 
                if (Loader.isLoading) {
                    readyListeners.push(listener);
                }
                else {
                    Ready.invoke(listener);
                }
            }
        },
 
        /**
         * @private
         * Ensure that any classes referenced in the `uses` property are loaded.
         */
        addUsedClasses: function(classes) {
            var cls, i, ln;
 
            if (classes) {
                classes = (typeof classes === 'string') ? [classes] : classes;
                
                for (= 0, ln = classes.length; i < ln; i++) {
                    cls = classes[i];
                    
                    if (typeof cls === 'string' && !Ext.Array.contains(usedClasses, cls)) {
                        usedClasses.push(cls);
                    }
                }
            }
 
            return Loader;
        },
 
        /**
         * @private
         */
        triggerReady: function() {
            var listener,
                refClasses = usedClasses;
 
            if (Loader.isLoading && refClasses.length) {
                // Empty the array to eliminate potential recursive loop issue
                usedClasses = [];
 
                // this may immediately call us back if all 'uses' classes
                // have been loaded
                Loader.require(refClasses);
            }
            else {
                // Must clear this before calling callbacks. This will cause any new loads
                // to call Ready.block() again. See below for more on this.
                Loader.isLoading = false;
 
                // These listeners are just those attached directly to Loader to wait for
                // class loading only.
                readyListeners.sort(Ready.sortFn);
 
                // this method can be called with Loader.isLoading either true or false
                // (can be called with false when all 'uses' classes are already loaded)
                // this may bypass the above if condition
                while (readyListeners.length && !Loader.isLoading) {
                    // we may re-enter triggerReady so we cannot necessarily iterate the
                    // readyListeners array
                    listener = readyListeners.pop();
                    Ready.invoke(listener);
                }
 
                // If the DOM is also ready, this will fire the normal onReady listeners.
                // An astute observer would note that we may now be back to isLoading and
                // so ask "Why you call unblock?". The reason is that we must match the
                // calls to block and since we transitioned from isLoading to !isLoading
                // here we must call unblock. If we have transitioned back to isLoading in
                // the above loop it will have called block again so the counter will be
                // increased and this call will not reduce the block count to 0. This is
                // done by loadScripts.
                Ready.unblock();
            }
        },
 
        /**
         * @private
         * @param {String} className 
         */
        historyPush: function(className) {
            if (className && !isInHistory[className] && !Manager.overrideMap[className]) {
                isInHistory[className] = true;
                history.push(className);
            }
            
            return Loader;
        },
 
        /**
         * This is an internal method that delegate content loading to the 
         * bootstrap layer.
         * @private
         * @param params
         */
        loadScripts: function(params) {
            var manifest = Ext.manifest,
                loadOrder = manifest && manifest.loadOrder,
                loadOrderMap = manifest && manifest.loadOrderMap,
                options;
            
            ++Loader.scriptsLoading;
            
            // if the load order map hasn't been created, create it now 
            // and cache on the manifest
            if (loadOrder && !loadOrderMap) {
                manifest.loadOrderMap = loadOrderMap = Boot.createLoadOrderMap(loadOrder);
            }
 
            // verify the loading state, as this may have transitioned us from
            // not loading to loading
            Loader.checkReady();
 
            options = Ext.apply({
                loadOrder: loadOrder,
                loadOrderMap: loadOrderMap,
                charset: _config.scriptCharset,
                success: Loader.onLoadSuccess,
                failure: Loader.onLoadFailure,
                sync: Loader.syncModeEnabled,
                _classNames: []
            }, params);
 
            options.userScope = options.scope;
            options.scope = options;
 
            Boot.load(options);
        },
 
        /**
         * This method is provide for use by the bootstrap layer.
         * @private
         * @param {String[]} urls 
         */
        loadScriptsSync: function(urls) {
            var syncwas = Loader.syncModeEnabled;
            
            Loader.syncModeEnabled = true;
            Loader.loadScripts({ url: urls });
            Loader.syncModeEnabled = syncwas;
        },
 
        /**
         * This method is provide for use by the bootstrap layer.
         * @private
         * @param {String[]} urls 
         */
        loadScriptsSyncBasePrefix: function(urls) {
            var syncwas = Loader.syncModeEnabled;
            
            Loader.syncModeEnabled = true;
            Loader.loadScripts({ url: urls, prependBaseUrl: true });
            Loader.syncModeEnabled = syncwas;
        },
        
        /**
         * Loads the specified script URL and calls the supplied callbacks. If this method
         * is called before {@link Ext#isReady}, the script's load will delay the transition
         * to ready. This can be used to load arbitrary scripts that may contain further
         * {@link Ext#require Ext.require} calls.
         *
         * @param {Object/String/String[]} options The options object or simply the URL(s) to load.
         * @param {String} options.url The URL from which to load the script.
         * @param {Function} [options.onLoad] The callback to call on successful load.
         * @param {Function} [options.onError] The callback to call on failure to load.
         * @param {Object} [options.scope] The scope (`this`) for the supplied callbacks.
         */
        loadScript: function(options) {
            var isString = typeof options === 'string',
                isArray = options instanceof Array,
                isObject = !isArray && !isString,
                url = isObject ? options.url : options,
                onError = isObject && options.onError,
                onLoad = isObject && options.onLoad,
                scope = isObject && options.scope,
                request = {
                    url: url,
                    scope: scope,
                    onLoad: onLoad,
                    onError: onError,
                    _classNames: []
                };
 
            Loader.loadScripts(request);
        },
 
        /**
         * @private
         */
        checkMissingQueue: function() {
            var missingQueue = Loader.missingQueue,
                newQueue = {},
                missing = 0,
                name;
            
            for (name in missingQueue) {
                // If class state is available for the name, that means ClassManager
                // is tracking it and will fire callback when it is created.
                // We only need to track non-class things in the Loader.
                if (!(Manager.classState[name] || Manager.isCreated(name))) {
                    newQueue[name] = missingQueue[name];
                    missing++;
                }
            }
            
            Loader.missingCount = missing;
            Loader.missingQueue = newQueue;
        },
 
        /**
         * @private
         */
        checkReady: function() {
            var wasLoading = Loader.isLoading,
                isLoading;
 
            Loader.checkMissingQueue();
            isLoading = Loader.missingCount + Loader.scriptsLoading;
            
            if (isLoading && !wasLoading) {
                Ready.block();
                Loader.isLoading = !!isLoading;
            }
            else if (!isLoading && wasLoading) {
                Loader.triggerReady();
            }
 
            //<debug>
            if (!Loader.scriptsLoading && Loader.missingCount) {
                // Things look bad, but since load requests may come later, defer this
                // for a bit then check if things are still stuck.
                Ext.defer(function() {
                    var name;
                    
                    if (!Loader.scriptsLoading && Loader.missingCount) {
                        Ext.log.error('[Loader] The following classes failed to load:');
                        
                        for (name in Loader.missingQueue) {
                            Ext.log.error('[Loader] ' + name + ' from ' +
                                Loader.missingQueue[name]);
                        }
                    }
                }, 1000);
            }
            //</debug>
        }
    });
 
    /**
     * Loads all classes by the given names and all their direct dependencies; optionally
     * executes the given callback function when finishes, within the optional scope.
     *
     * @param {String/String[]} expressions The class, classes or wildcards to load.
     * @param {Function} [fn] The callback function.
     * @param {Object} [scope] The execution scope (`this`) of the callback function.
     * @member Ext
     * @method require
     */
    Ext.require = alias(Loader, 'require');
 
    /**
     * Synchronously loads all classes by the given names and all their direct dependencies;
     * optionally executes the given callback function when finishes, within the optional scope.
     *
     * @param {String/String[]} expressions The class, classes or wildcards to load.
     * @param {Function} [fn] The callback function.
     * @param {Object} [scope] The execution scope (`this`) of the callback function.
     * @member Ext
     * @method syncRequire
     */
    Ext.syncRequire = alias(Loader, 'syncRequire');
 
    /**
     * Explicitly exclude files from being loaded. Useful when used in conjunction with a
     * broad include expression. Can be chained with more `require` and `exclude` methods,
     * for example:
     *
     *     Ext.exclude('Ext.data.*').require('*');
     *
     *     Ext.exclude('widget.button*').require('widget.*');
     *
     * @param {String/String[]} excludes
     * @return {Object} Contains `exclude`, `require` and `syncRequire` methods for chaining.
     * @member Ext
     * @method exclude
     */
    Ext.exclude = alias(Loader, 'exclude');
 
    //<feature classSystem.loader>
    /**
     * @cfg {String[]} requires
     * @member Ext.Class
     * List of classes that have to be loaded before instantiating this class.
     * For example:
     *
     *     Ext.define('Mother', {
     *         requires: ['Child'],
     *         giveBirth: function() {
     *             // we can be sure that child class is available.
     *             return new Child();
     *         }
     *     });
     */
    Class.registerPreprocessor('loader', function(cls, data, hooks, continueFn) {
        //<debug>
        Ext.classSystemMonitor &&
            Ext.classSystemMonitor(cls, 'Ext.Loader#loaderPreprocessor', arguments);
        //</debug>
        
        /* eslint-disable-next-line vars-on-top */
        var me = this,
            dependencies = [],
            dependency,
            className = Manager.getName(cls),
            i, j, ln, subLn, value, propertyName, propertyValue,
            requiredMap;
 
        /*
        Loop through the dependencyProperties, look for string class names and push
        them into a stack, regardless of whether the property's value is a string, array or object.
        For example:
        {
              extend: 'Ext.MyClass',
              requires: ['Ext.some.OtherClass'],
              mixins: {
                  thing: 'Foo.bar.Thing';
              }
        }
        which will later be transformed into:
        {
              extend: Ext.MyClass,
              requires: [Ext.some.OtherClass],
              mixins: {
                  thing: Foo.bar.Thing;
              }
        }
        */
 
        for (= 0, ln = dependencyProperties.length; i < ln; i++) {
            propertyName = dependencyProperties[i];
 
            if (data.hasOwnProperty(propertyName)) {
                propertyValue = data[propertyName];
 
                if (typeof propertyValue === 'string') {
                    dependencies.push(propertyValue);
                }
                else if (propertyValue instanceof Array) {
                    for (= 0, subLn = propertyValue.length; j < subLn; j++) {
                        value = propertyValue[j];
 
                        if (typeof value === 'string') {
                            dependencies.push(value);
                        }
                    }
                }
                else if (typeof propertyValue !== 'function') {
                    for (in propertyValue) {
                        if (propertyValue.hasOwnProperty(j)) {
                            value = propertyValue[j];
 
                            if (typeof value === 'string') {
                                dependencies.push(value);
                            }
                        }
                    }
                }
            }
        }
 
        if (dependencies.length === 0) {
            return;
        }
        
        if (className) {
            _requiresMap[className] = dependencies;
        }
 
        //<debug>
        /* eslint-disable-next-line vars-on-top */
        var manifestClasses = Ext.manifest && Ext.manifest.classes,
            deadlockPath = [],
            detectDeadlock;
 
        /*
         * Automatically detect deadlocks before-hand,
         * will throw an error with detailed path for ease of debugging. Examples
         * of deadlock cases:
         *
         *  - A extends B, then B extends A
         *  - A requires B, B requires C, then C requires A
         *
         * The detectDeadlock function will recursively transverse till the leaf, hence
         * it can detect deadlocks no matter how deep the path is. However we don't need
         * to run this check if the class name is in the manifest: that means Cmd has
         * already resolved all dependencies for this class with no deadlocks.
         */
 
        if (className && (!manifestClasses || !manifestClasses[className])) {
            requiredMap = Loader.requiredByMap || (Loader.requiredByMap = {});
 
            for (= 0, ln = dependencies.length; i < ln; i++) {
                dependency = dependencies[i];
                (requiredMap[dependency] || (requiredMap[dependency] = [])).push(className);
            }
 
            detectDeadlock = function(cls) {
                var requires = _requiresMap[cls],
                    dep, i, ln;
 
                deadlockPath.push(cls);
 
                if (requires) {
                    if (Ext.Array.contains(requires, className)) {
                        Ext.Error.raise("Circular requirement detected! '" + className +
                                "' and '" + deadlockPath[1] + "' mutually require each other. " +
                                "Path: " + deadlockPath.join(' -> ') + " -> " + deadlockPath[0]);
                    }
 
                    for (= 0, ln = requires.length; i < ln; i++) {
                        dep = requires[i];
                        
                        if (!isInHistory[dep]) {
                            detectDeadlock(requires[i]);
                        }
                    }
                }
            };
 
            detectDeadlock(className);
        }
        //</debug>
 
        (className ? Loader.exclude(className) : Loader).require(dependencies, function() {
            var i, ln, j, subLn, k;
            
            for (= 0, ln = dependencyProperties.length; i < ln; i++) {
                propertyName = dependencyProperties[i];
 
                if (data.hasOwnProperty(propertyName)) {
                    propertyValue = data[propertyName];
 
                    if (typeof propertyValue === 'string') {
                        data[propertyName] = Manager.get(propertyValue);
                    }
                    else if (propertyValue instanceof Array) {
                        for (= 0, subLn = propertyValue.length; j < subLn; j++) {
                            value = propertyValue[j];
 
                            if (typeof value === 'string') {
                                data[propertyName][j] = Manager.get(value);
                            }
                        }
                    }
                    else if (typeof propertyValue !== 'function') {
                        for (in propertyValue) {
                            if (propertyValue.hasOwnProperty(k)) {
                                value = propertyValue[k];
 
                                if (typeof value === 'string') {
                                    data[propertyName][k] = Manager.get(value);
                                }
                            }
                        }
                    }
                }
            }
 
            continueFn.call(me, cls, data, hooks);
        });
 
        return false;
    }, true, 'after', 'className');
 
    /**
     * @cfg {String[]} uses
     * @member Ext.Class
     * List of optional classes to load together with this class. These aren't neccessarily loaded
     * before this class is created, but are guaranteed to be available before Ext.onReady
     * listeners are invoked. For example:
     *
     *     Ext.define('Mother', {
     *         uses: ['Child'],
     *         giveBirth: function() {
     *             // This code might, or might not work:
     *             // return new Child();
     *
     *             // Instead use Ext.create() to load the class at the spot if not loaded already:
     *             return Ext.create('Child');
     *         }
     *     });
     */
    Manager.registerPostprocessor('uses', function(name, cls, data) {
        var uses = data.uses,
            classNames;
 
        //<debug>
        Ext.classSystemMonitor &&
            Ext.classSystemMonitor(cls, 'Ext.Loader#usesPostprocessor', arguments);
        //</debug>
        
        if (uses) {
            classNames = Manager.getNamesByExpression(data.uses);
            Loader.addUsedClasses(classNames);
        }
    });
 
    Manager.onCreated(Loader.historyPush);
    //</feature>
 
    Loader.init();
}());
 
//-----------------------------------------------------------------------------
 
// Use performance.now when available to keep timestamps consistent.
Ext._endTime = Ext.ticks();
 
// This hook is to allow tools like DynaTrace to deterministically detect the availability
// of Ext.onReady. Since Loader takes over Ext.onReady this must be done here and not in
// Ext.env.Ready.
if (Ext._beforereadyhandler) {
    Ext._beforereadyhandler();
}