Source: Tracker/plugins.js

/**
 * @class
 * @classdesc Plugins Management.
 * @name PluginsManager
 * @param tag {object} Instance of the Tag used
 * @public
 */
var PluginsManager = function (tag) {

    'use strict';

    var self = this;
    var pluginsLoaded = {};
    var _emit = 'Tracker:Plugin:Load:';

    /**
     * Dictionary indexed by plugin names indicating those currently being lazyloaded (true) and
     * those that were in this case (false).
     * example:
     * {
     *  'Page': true,
     *  'Click': false,
     *  'SalesTracker': true
     * }
     * Here Page, Click and SalesTracker are "Lazyloadable" and all were called. Page and SalesTracker are still loading
     * @private
     */
    var pluginsInLazyloading = {};

    /**
     * Gives the number of plugins loading
     * @private
     */
    var pluginsInLazyloadingCount = 0;

    /**
     * Dictionary indexed by plugin names indicating those that have one or more methods
     * which have been called and are waiting for execution (awaiting the end of plugin loading)
     * example:
     * {
     *  'Page': 3,
     *  'SalesTracker': 1
     * }
     * Page and SalesTracker have not yet been loaded but some of their methods have been called.
     * Three times for the page (this may be the same method) once for SalesTracker
     * @private
     */
    var ExecWaitingLazyloading = {};

    /**
     * Gives the number of methods waiting to be executed
     * @private
     */
    var ExecWaitingLazyloadingCount = 0;

    /**
     * Check if a plugin is loaded.
     * @memberof PluginsManager#
     * @param name {string} Plugin's name
     * @returns boolean {boolean} True if the plugin is loaded
     * @private
     */
    var _isLoaded = function (name) {
        var isLoaded = false;
        if (pluginsLoaded[name]) {
            isLoaded = true;
        }
        return isLoaded;
    };

    /**
     * Unload a plugin.
     * @name unload
     * @memberof PluginsManager#
     * @function
     * @param name {string} Plugin's name
     * @returns parent {object} Instance of the Tag used
     * @public
     */
    var _unload = self['unload'] = function (name) {
        if (_isLoaded(name)) {
            pluginsLoaded[name] = undefined;
            tag.emit('Tracker:Plugin:Unload:' + name + ':Ok', {lvl: 'INFO'});
        } else {
            tag.emit('Tracker:Plugin:Unload:' + name + ':Error', {lvl: 'ERROR', msg: 'not a known plugin'});
        }
        return tag;
    };

    /**
     * Mark a plugin loading as finished (if exists).
     * @memberof PluginsManager#
     * @param name {string} Plugin's name
     * @private
     */
    var _lazyloadingFinished = function (name) {
        if (pluginsInLazyloading[name] && _isLoaded(name)) {
            pluginsInLazyloading[name] = false;
            pluginsInLazyloadingCount--;
            if (_isLoaded(name + '_ll')) {
                _unload(name + '_ll');
            }
            if (pluginsInLazyloadingCount === 0) {
                tag.emit('Tracker:Plugin:Lazyload:File:Complete', {
                    lvl: 'INFO',
                    msg: 'LazyLoading triggers are finished'
                });
            }
        }
    };

    /**
     * Load a plugin.
     * @name load
     * @memberof PluginsManager#
     * @function
     * @param name {string} Plugin's name
     * @param Plugin {function} Plugin's code
     * @returns parent {object} Instance of the Tag used
     * @public
     */
    var _load = self['load'] = function (name, Plugin) {
        if (typeof Plugin === 'function') {
            var allowed = (typeof tag.getConfig.plgAllowed === 'undefined') ||
                (tag.getConfig.plgAllowed.length === 0) ||
                (tag.getConfig.plgAllowed.indexOf(name) > -1);
            if (allowed) {
                pluginsLoaded[name] = new Plugin(tag);
                _lazyloadingFinished(name);
                tag.emit(_emit + name + ':Ok', {lvl: 'INFO'});
            } else {
                tag.emit(_emit + name + ':Error', {
                    lvl: 'ERROR',
                    msg: 'Plugin not allowed',
                    details: {}
                });
            }
        } else {
            tag.emit(_emit + name + ':Error', {
                lvl: 'ERROR',
                msg: 'not a function',
                details: {obj: Plugin}
            });
        }
        return tag;
    };

    /**
     * Check if some plugin is loading.
     * @name isLazyloading
     * @memberof PluginsManager#
     * @function
     * @param name {string} Specific plugin's name
     * @returns true|false {boolean} True if one or the specific plugin is loading
     * @public
     */
    var _isLazyloading = self['isLazyloading'] = function (name) {
        if (name) {
            return pluginsInLazyloading[name] === true;
        } else {
            return pluginsInLazyloadingCount !== 0;
        }
    };

    /**
     * Indicate if the plugin is lazyloadable and not not loaded yet.
     * @memberof PluginsManager#
     * @param name {string} Plugin's name
     * @returns {boolean}
     * @private
     */
    var _isLazyLoadable = function (name) {
        return !_isLoaded(name) && !_isLazyloading(name) && _isLoaded(name + '_ll');
    };

    /**
     * Ask the lazyloading of a plugin.
     * @memberof PluginsManager#
     * @param name {string} Plugin name
     * @private
     */
    var _lazyLoad = function (name) {
        pluginsInLazyloading[name] = true;
        pluginsInLazyloadingCount++;
        ATInternet.Utils.loadScript({url: tag.getConfig('lazyLoadingPath') + name + '.js'});
    };

    /**
     * Lazyload a plugin if possible.
     * @memberof PluginsManager#
     * @param name {string} Plugin's name
     * @returns true|false {boolean} False if the plugin can't be loaded (not planned for, already loaded or already in progress).
     * @private
     */
    var _lazyLoadIfPossible = function (name) {
        if (_isLazyLoadable(name)) {
            _lazyLoad(name);
            return true;
        } else {
            return false;
        }
    };

    /**
     * Add a marker showing the method's call of a plugin which is lazyloading.
     * @memberof PluginsManager#
     * @param plg {string} Plugin's name
     * @private
     */
    var _addExecWaitingLazyloading = function (plg) {
        if (!ExecWaitingLazyloading[plg]) {
            ExecWaitingLazyloading[plg] = 1;
        } else {
            ExecWaitingLazyloading[plg]++;
        }
        ExecWaitingLazyloadingCount++;
    };

    /**
     * Delete a marker showing the method's call of a plugin which is lazyloading.
     * @memberof PluginsManager#
     * @param plg {string} Plugin name
     * @private
     */
    var _delExecWaitingLazyloading = function (plg) {
        ExecWaitingLazyloading[plg]--;
        ExecWaitingLazyloadingCount--;
        if (ExecWaitingLazyloadingCount === 0) {
            tag.emit('Tracker:Plugin:Lazyload:Exec:Complete', {
                lvl: 'INFO',
                msg: 'All exec waiting for lazyloading are done'
            });
        }
    };

    /**
     * Process lazyloading.
     * @memberof PluginsManager#
     * @param pluginName {string} Plugin (name) that you want to use
     * @param method {string} Plugin's method that you want to use
     * @param args {Array} Arguments to the method you want to use
     * @param callback {function} Method which be triggered once the plugin's method has finished, it will take one argument which is what the plugin's method returned
     * @private
     */
    var _process = function (pluginName, method, args, callback) {
        var result = null;
        var tb = method.split('.');
        if (_isLoaded(pluginName) && pluginsLoaded[pluginName][tb[0]]) {
            if ((tb.length > 1) && pluginsLoaded[pluginName][tb[0]][tb[1]]) {
                result = pluginsLoaded[pluginName][tb[0]][tb[1]].apply(pluginsLoaded[pluginName], args);
            } else {
                result = pluginsLoaded[pluginName][tb[0]].apply(pluginsLoaded[pluginName], args);
            }
        }
        callback && callback(result);
    };

    /**
     * Process lazyloading.
     * @memberof PluginsManager#
     * @param pluginName {string} Plugin (name) that you want to use
     * @param method {string} Plugin's method that you want to use
     * @param args {Array} Arguments to the method you want to use
     * @param callback {function} Method which be triggered once the plugin's method has finished, it will take one argument which is what the plugin's method returned
     * @private
     */
    var _waitingLazyloadingProcess = function (pluginName, method, args, callback) {
        _addExecWaitingLazyloading(pluginName);
        tag.onTrigger(_emit + pluginName + ':Ok', function () {
            _process(pluginName, method, args, function (result) {
                _delExecWaitingLazyloading(pluginName);
                callback && callback(result);
            });
        }, true);
    };

    /**
     * Get missing dependencies.
     * @memberof PluginsManager#
     * @param listDep {array} Dependency name
     * @private
     */
    var _missingDependencies = function (listDep) {
        var missing = {'mcount': 0, 'plugins': {}};
        for (var i = 0; i < listDep.length; i++) {
            if (!pluginsLoaded.hasOwnProperty(listDep[i])) {
                missing.mcount++;
                missing.plugins[listDep[i]] = true;
            }
        }
        return missing;
    };

    /**
     * Indicate that at least one method is waiting the end of its plugin's lazyloading.
     * @name isExecWaitingLazyloading
     * @memberof PluginsManager#
     * @function
     * @returns {boolean}
     * @public
     */
    self.isExecWaitingLazyloading = function () {
        return ExecWaitingLazyloadingCount !== 0;
    };

    /**
     * Execute a plugin method.
     * @name exec
     * @memberof PluginsManager#
     * @function
     * @param pluginName {string} Plugin (name) that you want to use
     * @param method {string} Plugin's method that you want to use
     * @param args {Array} Arguments to the method you want to use
     * @param callback {function} Method which be triggered once the plugin's method has finished, it will take one argument which is what the plugin's method returned
     * @public
     */
    tag.exec = self.exec = function (pluginName, method, args, callback) {
        if (_isLazyLoadable(pluginName)) {
            _waitingLazyloadingProcess(pluginName, method, args, callback);
            _lazyLoad(pluginName);
        } else if (_isLazyloading(pluginName)) {
            _waitingLazyloadingProcess(pluginName, method, args, callback);
        } else {
            _process(pluginName, method, args, callback);
        }
    };

    /**
     * Execute a callback only when all required plugins are loaded.
     * @name waitForDependencies
     * @memberof PluginsManager#
     * @function
     * @param dependencies {Array} Names of the required plugins
     * @param callback {function} Method which be triggered once the dependencies will be loaded
     * @public
     */
    self.waitForDependencies = function (dependencies, callback) {
        var missing = _missingDependencies(dependencies);
        if (missing.mcount === 0) {
            tag.emit('Tracker:Plugin:Dependencies:Loaded', {
                lvl: 'INFO',
                details: {dependencies: dependencies}
            });
            callback();
        } else {
            for (var name in missing.plugins) {
                if (missing.plugins.hasOwnProperty(name)) {
                    tag.emit('Tracker:Plugin:Dependencies:Error', {
                        lvl: 'WARNING',
                        msg: 'Missing plugin ' + name
                    });
                    // On s'inscrit sur l'évènement de chargement du plugin manquant
                    tag.onTrigger(_emit + name, function (trig, data) {
                        var trigArr = trig.split(":");
                        var plugName = trigArr[3];
                        var state = trigArr[4];
                        if (state === 'Ok') {
                            missing.plugins[plugName] = false;
                            missing.mcount--;
                            // Si tous les plugins attendus ont été chargés, on exécute le callback
                            if (missing.mcount === 0) {
                                callback();
                            }
                        }
                    }, true);
                    // Si le plugins est lazyloadable (et qu'il n'est pas en cours de lazyloading) on demande son chargement
                    _lazyLoadIfPossible(name);
                }
            }
        }
    };

    /**
     * Create an instance of each plugin already in the prototype.
     * Essential for the inclusion of the plugins already loaded.
     * @name init
     * @memberof PluginsManager#
     * @function
     * @public
     */
    self.init = function () {
        for (var key in ATInternet.Tracker.pluginProtos) {
            if (ATInternet.Tracker.pluginProtos.hasOwnProperty(key)) {
                _load(key, ATInternet.Tracker.pluginProtos[key]);
            }
        }
    };

    /* @if test */
    self._isLoaded = _isLoaded;
    self._unload = _unload;
    self._lazyloadingFinished = _lazyloadingFinished;
    self._load = _load;
    self._isLazyloading = _isLazyloading;
    self._isLazyLoadable = _isLazyLoadable;
    self._lazyLoad = _lazyLoad;
    self._lazyLoadIfPossible = _lazyLoadIfPossible;
    self._addExecWaitingLazyloading = _addExecWaitingLazyloading;
    self._delExecWaitingLazyloading = _delExecWaitingLazyloading;
    self._process = _process;
    self._waitingLazyloadingProcess = _waitingLazyloadingProcess;
    self._missingDependencies = _missingDependencies;
    self._pluginsLoaded = function () {
        return pluginsLoaded;
    };
    /* @endif */
};