/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2018 Photon Storm Ltd.
 * @license      {@link https://github.com/photonstorm/phaser/blob/master/license.txt|MIT License}
 */

var Class = require('../utils/Class');
var EventEmitter = require('eventemitter3');
var FileTypesManager = require('../loader/FileTypesManager');
var GameObjectCreator = require('../gameobjects/GameObjectCreator');
var GameObjectFactory = require('../gameobjects/GameObjectFactory');
var GetFastValue = require('../utils/object/GetFastValue');
var PluginCache = require('./PluginCache');
var Remove = require('../utils/array/Remove');

/**
 * @typedef {object} GlobalPlugin
 *
 * @property {string} key - The unique name of this plugin within the plugin cache.
 * @property {function} plugin - An instance of the plugin.
 * @property {boolean} [active] - Is the plugin active or not?
 * @property {string} [mapping] - If this plugin is to be injected into the Scene Systems, this is the property key map used.
 */

/**
 * @classdesc
 * The PluginManager is responsible for installing and adding plugins to Phaser.
 *
 * It is a global system and therefore belongs to the Game instance, not a specific Scene.
 *
 * It works in conjunction with the PluginCache. Core internal plugins automatically register themselves 
 * with the Cache, but it's the Plugin Manager that is responsible for injecting them into the Scenes.
 *
 * There are two types of plugin:
 *
 * 1) A Global Plugin
 * 2) A Scene Plugin
 *
 * A Global Plugin is a plugin that lives within the Plugin Manager rather than a Scene. You can get
 * access to it by calling `PluginManager.get` and providing a key. Any Scene that requests a plugin in
 * this way will all get access to the same plugin instance, allowing you to use a single plugin across
 * multiple Scenes.
 *
 * A Scene Plugin is a plugin dedicated to running within a Scene. These are different to Global Plugins
 * in that their instances do not live within the Plugin Manager, but within the Scene Systems class instead.
 * And that every Scene created is given its own unique instance of a Scene Plugin. Examples of core Scene
 * Plugins include the Input Plugin, the Tween Plugin and the physics Plugins.
 *
 * You can add a plugin to Phaser in three different ways:
 *
 * 1) Preload it
 * 2) Include it in your source code and install it via the Game Config
 * 3) Include it in your source code and install it within a Scene
 *
 * For examples of all of these approaches please see the Phaser 3 Examples Repo `plugins` folder.
 *
 * For information on creating your own plugin please see the Phaser 3 Plugin Template.
 *
 * @class PluginManager
 * @memberOf Phaser.Plugins
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Game} game - The game instance that owns this Plugin Manager.
 */
var PluginManager = new Class({

    Extends: EventEmitter,

    initialize:

    function PluginManager (game)
    {
        EventEmitter.call(this);

        /**
         * The game instance that owns this Plugin Manager.
         *
         * @name Phaser.Plugins.PluginManager#game
         * @type {Phaser.Game}
         * @since 3.0.0
         */
        this.game = game;

        /**
         * The global plugins currently running and managed by this Plugin Manager.
         * A plugin must have been started at least once in order to appear in this list.
         *
         * @name Phaser.Plugins.PluginManager#plugins
         * @type {GlobalPlugin[]}
         * @since 3.8.0
         */
        this.plugins = [];

        /**
         * A list of plugin keys that should be installed into Scenes as well as the Core Plugins.
         *
         * @name Phaser.Plugins.PluginManager#scenePlugins
         * @type {string[]}
         * @since 3.8.0
         */
        this.scenePlugins = [];

        /**
         * A temporary list of plugins to install when the game has booted.
         *
         * @name Phaser.Plugins.PluginManager#_pendingGlobal
         * @private
         * @type {array}
         * @since 3.8.0
         */
        this._pendingGlobal = [];

        /**
         * A temporary list of scene plugins to install when the game has booted.
         *
         * @name Phaser.Plugins.PluginManager#_pendingScene
         * @private
         * @type {array}
         * @since 3.8.0
         */
        this._pendingScene = [];

        if (game.isBooted)
        {
            this.boot();
        }
        else
        {
            game.events.once('boot', this.boot, this);
        }
    },

    /**
     * Run once the game has booted and installs all of the plugins configured in the Game Config.
     *
     * @method Phaser.Plugins.PluginManager#boot
     * @protected
     * @since 3.0.0
     */
    boot: function ()
    {
        var i;
        var entry;
        var key;
        var plugin;
        var start;
        var mapping;
        var data;
        var config = this.game.config;

        //  Any plugins to install?
        var list = config.installGlobalPlugins;

        //  Any plugins added outside of the game config, but before the game booted?
        list = list.concat(this._pendingGlobal);

        for (i = 0; i < list.length; i++)
        {
            entry = list[i];

            // { key: 'TestPlugin', plugin: TestPlugin, start: true, mapping: 'test', data: { msg: 'The plugin is alive' } }

            key = GetFastValue(entry, 'key', null);
            plugin = GetFastValue(entry, 'plugin', null);
            start = GetFastValue(entry, 'start', false);
            mapping = GetFastValue(entry, 'mapping', null);
            data = GetFastValue(entry, 'data', null);

            if (key && plugin)
            {
                this.install(key, plugin, start, mapping, data);
            }
        }

        //  Any scene plugins to install?
        list = config.installScenePlugins;

        //  Any plugins added outside of the game config, but before the game booted?
        list = list.concat(this._pendingScene);

        for (i = 0; i < list.length; i++)
        {
            entry = list[i];

            // { key: 'moveSpritePlugin', plugin: MoveSpritePlugin, , mapping: 'move' }

            key = GetFastValue(entry, 'key', null);
            plugin = GetFastValue(entry, 'plugin', null);
            mapping = GetFastValue(entry, 'mapping', null);

            if (key && plugin)
            {
                this.installScenePlugin(key, plugin, mapping);
            }
        }

        this._pendingGlobal = [];
        this._pendingScene = [];

        this.game.events.once('destroy', this.destroy, this);
    },

    /**
     * Called by the Scene Systems class. Tells the plugin manager to install all Scene plugins into it.
     *
     * First it will install global references, i.e. references from the Game systems into the Scene Systems (and Scene if mapped.)
     * Then it will install Core Scene Plugins followed by Scene Plugins registered with the PluginManager.
     * Finally it will install any references to Global Plugins that have a Scene mapping property into the Scene itself.
     *
     * @method Phaser.Plugins.PluginManager#addToScene
     * @protected
     * @since 3.8.0
     *
     * @param {Phaser.Scenes.Systems} sys - The Scene Systems class to install all the plugins in to.
     * @param {array} globalPlugins - An array of global plugins to install.
     * @param {array} scenePlugins - An array of scene plugins to install.
     */
    addToScene: function (sys, globalPlugins, scenePlugins)
    {
        var i;
        var pluginKey;
        var pluginList;
        var game = this.game;
        var scene = sys.scene;
        var map = sys.settings.map;
        var isBooted = sys.settings.isBooted;

        //  Reference the GlobalPlugins from Game into Scene.Systems
        for (i = 0; i < globalPlugins.length; i++)
        {
            pluginKey = globalPlugins[i];
           
            if (game[pluginKey])
            {
                sys[pluginKey] = game[pluginKey];

                //  Scene level injection
                if (map.hasOwnProperty(pluginKey))
                {
                    scene[map[pluginKey]] = sys[pluginKey];
                }
            }
            else if (pluginKey === 'game' && map.hasOwnProperty(pluginKey))
            {
                scene[map[pluginKey]] = game;
            }
        }

        for (var s = 0; s < scenePlugins.length; s++)
        {
            pluginList = scenePlugins[s];

            for (i = 0; i < pluginList.length; i++)
            {
                pluginKey = pluginList[i];

                if (!PluginCache.hasCore(pluginKey))
                {
                    continue;
                }

                var source = PluginCache.getCore(pluginKey);

                var plugin = new source.plugin(scene, this);
                
                sys[source.mapping] = plugin;

                //  Scene level injection
                if (source.custom)
                {
                    scene[source.mapping] = plugin;
                }
                else if (map.hasOwnProperty(source.mapping))
                {
                    scene[map[source.mapping]] = plugin;
                }

                //  Scene is already booted, usually because this method is being called at run-time, so boot the plugin
                if (isBooted)
                {
                    plugin.boot();
                }
            }
        }

        //  And finally, inject any 'global scene plugins'
        pluginList = this.plugins;

        for (i = 0; i < pluginList.length; i++)
        {
            var entry = pluginList[i];
           
            if (entry.mapping)
            {
                scene[entry.mapping] = entry.plugin;
            }
        }
    },

    /**
     * Called by the Scene Systems class. Returns a list of plugins to be installed.
     *
     * @method Phaser.Plugins.PluginManager#getDefaultScenePlugins
     * @protected
     * @since 3.8.0
     *
     * @return {string[]} A list keys of all the Scene Plugins to install.
     */
    getDefaultScenePlugins: function ()
    {
        var list = this.game.config.defaultPlugins;

        //  Merge in custom Scene plugins
        list = list.concat(this.scenePlugins);

        return list;
    },

    /**
     * Installs a new Scene Plugin into the Plugin Manager and optionally adds it
     * to the given Scene as well. A Scene Plugin added to the manager in this way
     * will be automatically installed into all new Scenes using the key and mapping given.
     *
     * The `key` property is what the plugin is injected into Scene.Systems as.
     * The `mapping` property is optional, and if specified is what the plugin is installed into
     * the Scene as. For example:
     *
     * ```javascript
     * this.plugins.installScenePlugin('powerupsPlugin', pluginCode, 'powerups');
     * 
     * // and from within the scene:
     * this.sys.powerupsPlugin; // key value
     * this.powerups; // mapping value
     * ```
     *
     * This method is called automatically by Phaser if you install your plugins using either the
     * Game Configuration object, or by preloading them via the Loader.
     *
     * @method Phaser.Plugins.PluginManager#installScenePlugin
     * @since 3.8.0
     *
     * @param {string} key - The property key that will be used to add this plugin to Scene.Systems.
     * @param {function} plugin - The plugin code. This should be the non-instantiated version.
     * @param {string} [mapping] - If this plugin is injected into the Phaser.Scene class, this is the property key to use.
     * @param {Phaser.Scene} [addToScene] - Optionally automatically add this plugin to the given Scene.
     */
    installScenePlugin: function (key, plugin, mapping, addToScene)
    {
        if (typeof plugin !== 'function')
        {
            console.warn('Invalid Scene Plugin: ' + key);
            return;
        }

        if (PluginCache.hasCore(key))
        {
            console.warn('Scene Plugin key in use: ' + key);
            return;
        }

        PluginCache.register(key, plugin, mapping, true);

        this.scenePlugins.push(key);

        if (addToScene)
        {
            var instance = new plugin(addToScene, this);

            addToScene.sys[key] = instance;

            if (mapping && mapping !== '')
            {
                addToScene[mapping] = instance;
            }

            instance.boot();
        }
    },

    /**
     * Installs a new Global Plugin into the Plugin Manager and optionally starts it running.
     * A global plugin belongs to the Plugin Manager, rather than a specific Scene, and can be accessed
     * and used by all Scenes in your game.
     *
     * The `key` property is what you use to access this plugin from the Plugin Manager.
     *
     * ```javascript
     * this.plugins.install('powerupsPlugin', pluginCode);
     * 
     * // and from within the scene:
     * this.plugins.get('powerupsPlugin');
     * ```
     *
     * This method is called automatically by Phaser if you install your plugins using either the
     * Game Configuration object, or by preloading them via the Loader.
     *
     * The same plugin can be installed multiple times into the Plugin Manager by simply giving each
     * instance its own unique key.
     *
     * @method Phaser.Plugins.PluginManager#install
     * @since 3.8.0
     * 
     * @param {string} key - The unique handle given to this plugin within the Plugin Manager.
     * @param {function} plugin - The plugin code. This should be the non-instantiated version.
     * @param {boolean} [start=false] - Automatically start the plugin running? This is always `true` if you provide a mapping value.
     * @param {string} [mapping] - If this plugin is injected into the Phaser.Scene class, this is the property key to use.
     * @param {any} [data] - A value passed to the plugin's `init` method.
     */
    install: function (key, plugin, start, mapping, data)
    {
        if (start === undefined) { start = false; }
        if (mapping === undefined) { mapping = null; }
        if (data === undefined) { data = null; }

        if (typeof plugin !== 'function')
        {
            console.warn('Invalid Plugin: ' + key);
            return;
        }

        if (PluginCache.hasCustom(key))
        {
            console.warn('Plugin key in use: ' + key);
            return;
        }

        if (mapping !== null)
        {
            start = true;
        }

        if (!this.game.isBooted)
        {
            this._pendingGlobal.push({ key: key, plugin: plugin, start: start, mapping: mapping, data: data });
        }
        else
        {
            //  Add it to the plugin store
            PluginCache.registerCustom(key, plugin, mapping, data);

            if (start)
            {
                return this.start(key);
            }
        }
    },

    /**
     * Gets an index of a global plugin based on the given key.
     *
     * @method Phaser.Plugins.PluginManager#getIndex
     * @protected
     * @since 3.8.0
     *
     * @param {string} key - The unique plugin key.
     *
     * @return {integer} The index of the plugin within the plugins array.
     */
    getIndex: function (key)
    {
        var list = this.plugins;

        for (var i = 0; i < list.length; i++)
        {
            var entry = list[i];

            if (entry.key === key)
            {
                return i;
            }
        }

        return -1;
    },

    /**
     * Gets a global plugin based on the given key.
     *
     * @method Phaser.Plugins.PluginManager#getEntry
     * @protected
     * @since 3.8.0
     *
     * @param {string} key - The unique plugin key.
     *
     * @return {GlobalPlugin} The plugin entry.
     */
    getEntry: function (key)
    {
        var idx = this.getIndex(key);

        if (idx !== -1)
        {
            return this.plugins[idx];
        }
    },

    /**
     * Checks if the given global plugin, based on its key, is active or not.
     *
     * @method Phaser.Plugins.PluginManager#isActive
     * @since 3.8.0
     *
     * @param {string} key - The unique plugin key.
     *
     * @return {boolean} `true` if the plugin is active, otherwise `false`.
     */
    isActive: function (key)
    {
        var entry = this.getEntry(key);

        return (entry && entry.active);
    },

    /**
     * Starts a global plugin running.
     *
     * If the plugin was previously active then calling `start` will reset it to an active state and then
     * call its `start` method.
     *
     * If the plugin has never been run before a new instance of it will be created within the Plugin Manager,
     * its active state set and then both of its `init` and `start` methods called, in that order.
     *
     * If the plugin is already running under the given key then nothing happens.
     *
     * @method Phaser.Plugins.PluginManager#start
     * @since 3.8.0
     *
     * @param {string} key - The key of the plugin to start.
     * @param {string} [runAs] - Run the plugin under a new key. This allows you to run one plugin multiple times.
     *
     * @return {?Phaser.Plugins.BasePlugin} The plugin that was started, or `null` if invalid key given or plugin is already stopped.
     */
    start: function (key, runAs)
    {
        if (runAs === undefined) { runAs = key; }

        var entry = this.getEntry(runAs);

        //  Plugin already running under this key?
        if (entry && !entry.active)
        {
            //  It exists, we just need to start it up again
            entry.active = true;
            entry.plugin.start();
        }
        else if (!entry)
        {
            entry = this.createEntry(key, runAs);
        }

        return (entry) ? entry.plugin : null;
    },

    /**
     * Creates a new instance of a global plugin, adds an entry into the plugins array and returns it.
     *
     * @method Phaser.Plugins.PluginManager#createEntry
     * @private
     * @since 3.9.0
     *
     * @param {string} key - The key of the plugin to create an instance of.
     * @param {string} [runAs] - Run the plugin under a new key. This allows you to run one plugin multiple times.
     *
     * @return {?Phaser.Plugins.BasePlugin} The plugin that was started, or `null` if invalid key given.
     */
    createEntry: function (key, runAs)
    {
        var entry = PluginCache.getCustom(key);

        if (entry)
        {
            var instance = new entry.plugin(this);

            entry = {
                key: runAs,
                plugin: instance,
                active: true,
                mapping: entry.mapping,
                data: entry.data
            };

            this.plugins.push(entry);

            instance.init(entry.data);
            instance.start();
        }

        return entry;
    },

    /**
     * Stops a global plugin from running.
     *
     * If the plugin is active then its active state will be set to false and the plugins `stop` method
     * will be called.
     *
     * If the plugin is not already running, nothing will happen.
     *
     * @method Phaser.Plugins.PluginManager#stop
     * @since 3.8.0
     *
     * @param {string} key - The key of the plugin to stop.
     *
     * @return {Phaser.Plugins.PluginManager} The Plugin Manager.
     */
    stop: function (key)
    {
        var entry = this.getEntry(key);

        if (entry && entry.active)
        {
            entry.active = false;
            entry.plugin.stop();
        }

        return this;
    },

    /**
     * Gets a global plugin from the Plugin Manager based on the given key and returns it.
     *
     * If it cannot find an active plugin based on the key, but there is one in the Plugin Cache with the same key,
     * then it will create a new instance of the cached plugin and return that.
     *
     * @method Phaser.Plugins.PluginManager#get
     * @since 3.8.0
     *
     * @param {string} key - The key of the plugin to get.
     * @param {boolean} [autoStart=true] - Automatically start a new instance of the plugin if found in the cache, but not actively running.
     *
     * @return {?(Phaser.Plugins.BasePlugin|function)} The plugin, or `null` if no plugin was found matching the key.
     */
    get: function (key, autoStart)
    {
        if (autoStart === undefined) { autoStart = true; }

        var entry = this.getEntry(key);

        if (entry)
        {
            return entry.plugin;
        }
        else
        {
            var plugin = this.getClass(key);

            if (plugin && autoStart)
            {
                entry = this.createEntry(key, key);

                return (entry) ? entry.plugin : null;
            }
            else if (plugin)
            {
                return plugin;
            }
        }

        return null;
    },

    /**
     * Returns the plugin class from the cache.
     * Used internally by the Plugin Manager.
     *
     * @method Phaser.Plugins.PluginManager#getClass
     * @since 3.8.0
     *
     * @param {string} key - The key of the plugin to get.
     *
     * @return {Phaser.Plugins.BasePlugin} A Plugin object
     */
    getClass: function (key)
    {
        return PluginCache.getCustomClass(key);
    },

    /**
     * Removes a global plugin from the Plugin Manager and Plugin Cache.
     *
     * It is up to you to remove all references to this plugin that you may hold within your game code.
     *
     * @method Phaser.Plugins.PluginManager#removeGlobalPlugin
     * @since 3.8.0
     *
     * @param {string} key - The key of the plugin to remove.
     */
    removeGlobalPlugin: function (key)
    {
        var entry = this.getEntry(key);

        if (entry)
        {
            Remove(this.plugins, entry);
        }

        PluginCache.removeCustom(key);
    },

    /**
     * Removes a scene plugin from the Plugin Manager and Plugin Cache.
     *
     * This will not remove the plugin from any active Scenes that are already using it.
     *
     * It is up to you to remove all references to this plugin that you may hold within your game code.
     *
     * @method Phaser.Plugins.PluginManager#removeScenePlugin
     * @since 3.8.0
     *
     * @param {string} key - The key of the plugin to remove.
     */
    removeScenePlugin: function (key)
    {
        Remove(this.scenePlugins, key);

        PluginCache.remove(key);
    },

    /**
     * Registers a new type of Game Object with the global Game Object Factory and / or Creator.
     * This is usually called from within your Plugin code and is a helpful short-cut for creating
     * new Game Objects.
     *
     * The key is the property that will be injected into the factories and used to create the
     * Game Object. For example:
     *
     * ```javascript
     * this.plugins.registerGameObject('clown', clownFactoryCallback, clownCreatorCallback);
     * // later in your game code:
     * this.add.clown();
     * this.make.clown();
     * ```
     * 
     * The callbacks are what are called when the factories try to create a Game Object
     * matching the given key. It's important to understand that the callbacks are invoked within
     * the context of the GameObjectFactory. In this context there are several properties available
     * to use:
     * 
     * this.scene - A reference to the Scene that owns the GameObjectFactory.
     * this.displayList - A reference to the Display List the Scene owns.
     * this.updateList - A reference to the Update List the Scene owns.
     * 
     * See the GameObjectFactory and GameObjectCreator classes for more details.
     * Any public property or method listed is available from your callbacks under `this`.
     *
     * @method Phaser.Plugins.PluginManager#registerGameObject
     * @since 3.8.0
     *
     * @param {string} key - The key of the Game Object that the given callbacks will create, i.e. `image`, `sprite`.
     * @param {function} [factoryCallback] - The callback to invoke when the Game Object Factory is called.
     * @param {function} [creatorCallback] - The callback to invoke when the Game Object Creator is called.
     */
    registerGameObject: function (key, factoryCallback, creatorCallback)
    {
        if (factoryCallback)
        {
            GameObjectFactory.register(key, factoryCallback);
        }

        if (creatorCallback)
        {
            GameObjectCreator.register(key, creatorCallback);
        }

        return this;
    },

    /**
     * Registers a new file type with the global File Types Manager, making it available to all Loader
     * Plugins created after this.
     * 
     * This is usually called from within your Plugin code and is a helpful short-cut for creating
     * new loader file types.
     *
     * The key is the property that will be injected into the Loader Plugin and used to load the
     * files. For example:
     *
     * ```javascript
     * this.plugins.registerFileType('wad', doomWadLoaderCallback);
     * // later in your preload code:
     * this.load.wad();
     * ```
     * 
     * The callback is what is called when the loader tries to load a file  matching the given key.
     * It's important to understand that the callback is invoked within
     * the context of the LoaderPlugin. In this context there are several properties / methods available
     * to use:
     * 
     * this.addFile - A method to add the new file to the load queue.
     * this.scene - The Scene that owns the Loader Plugin instance.
     *
     * See the LoaderPlugin class for more details. Any public property or method listed is available from
     * your callback under `this`.
     *
     * @method Phaser.Plugins.PluginManager#registerFileType
     * @since 3.8.0
     *
     * @param {string} key - The key of the Game Object that the given callbacks will create, i.e. `image`, `sprite`.
     * @param {function} callback - The callback to invoke when the Game Object Factory is called.
     * @param {Phaser.Scene} [addToScene] - Optionally add this file type into the Loader Plugin owned by the given Scene.
     */
    registerFileType: function (key, callback, addToScene)
    {
        FileTypesManager.register(key, callback);

        if (addToScene && addToScene.sys.load)
        {
            addToScene.sys.load[key] = callback;
        }
    },

    /**
     * Destroys this Plugin Manager and all associated plugins.
     * It will iterate all plugins found and call their `destroy` methods.
     * 
     * The PluginCache will remove all custom plugins.
     *
     * @method Phaser.Plugins.PluginManager#destroy
     * @since 3.8.0
     */
    destroy: function ()
    {
        for (var i = 0; i < this.plugins.length; i++)
        {
            this.plugins[i].plugin.destroy();
        }

        PluginCache.destroyCustomPlugins();

        if (this.game.noReturn)
        {
            PluginCache.destroyCorePlugins();
        }

        this.game = null;
        this.plugins = [];
        this.scenePlugins = [];
    }

});

/*
 * "Sometimes, the elegant implementation is just a function.
 * Not a method. Not a class. Not a framework. Just a function."
 *  -- John Carmack
 */

module.exports = PluginManager;
