import { loadScript } from "../utils/loader.js";

/**
 * Manages loading and registering of reveal.js plugins.
 */
export default class Plugins {
    constructor(reveal) {
        this.Reveal = reveal;

        // Flags our current state (idle -> loading -> loaded)
        this.state = "idle";

        // An id:instance map of currently registered plugins
        this.registeredPlugins = {};

        this.asyncDependencies = [];
    }

    /**
     * Loads reveal.js dependencies, registers and
     * initializes plugins.
     *
     * Plugins are direct references to a reveal.js plugin
     * object that we register and initialize after any
     * synchronous dependencies have loaded.
     *
     * Dependencies are defined via the 'dependencies' config
     * option and will be loaded prior to starting reveal.js.
     * Some dependencies may have an 'async' flag, if so they
     * will load after reveal.js has been started up.
     */
    load(plugins, dependencies) {
        this.state = "loading";

        plugins.forEach(this.registerPlugin.bind(this));

        return new Promise((resolve) => {
            let scripts = [],
                scriptsToLoad = 0;

            dependencies.forEach((s) => {
                // Load if there's no condition or the condition is truthy
                if (!s.condition || s.condition()) {
                    if (s.async) {
                        this.asyncDependencies.push(s);
                    } else {
                        scripts.push(s);
                    }
                }
            });

            if (scripts.length) {
                scriptsToLoad = scripts.length;

                const scriptLoadedCallback = (s) => {
                    if (s && typeof s.callback === "function") s.callback();

                    if (--scriptsToLoad === 0) {
                        this.initPlugins().then(resolve);
                    }
                };

                // Load synchronous scripts
                scripts.forEach((s) => {
                    if (typeof s.id === "string") {
                        this.registerPlugin(s);
                        scriptLoadedCallback(s);
                    } else if (typeof s.src === "string") {
                        loadScript(s.src, () => scriptLoadedCallback(s));
                    } else {
                        console.warn("Unrecognized plugin format", s);
                        scriptLoadedCallback();
                    }
                });
            } else {
                this.initPlugins().then(resolve);
            }
        });
    }

    /**
     * Initializes our plugins and waits for them to be ready
     * before proceeding.
     */
    initPlugins() {
        return new Promise((resolve) => {
            let pluginValues = Object.values(this.registeredPlugins);
            let pluginsToInitialize = pluginValues.length;

            // If there are no plugins, skip this step
            if (pluginsToInitialize === 0) {
                this.loadAsync().then(resolve);
            }
            // ... otherwise initialize plugins
            else {
                let initNextPlugin;

                let afterPlugInitialized = () => {
                    if (--pluginsToInitialize === 0) {
                        this.loadAsync().then(resolve);
                    } else {
                        initNextPlugin();
                    }
                };

                let i = 0;

                // Initialize plugins serially
                initNextPlugin = () => {
                    let plugin = pluginValues[i++];

                    // If the plugin has an 'init' method, invoke it
                    if (typeof plugin.init === "function") {
                        let promise = plugin.init(this.Reveal);

                        // If the plugin returned a Promise, wait for it
                        if (promise && typeof promise.then === "function") {
                            promise.then(afterPlugInitialized);
                        } else {
                            afterPlugInitialized();
                        }
                    } else {
                        afterPlugInitialized();
                    }
                };

                initNextPlugin();
            }
        });
    }

    /**
     * Loads all async reveal.js dependencies.
     */
    loadAsync() {
        this.state = "loaded";

        if (this.asyncDependencies.length) {
            this.asyncDependencies.forEach((s) => {
                loadScript(s.src, s.callback);
            });
        }

        return Promise.resolve();
    }

    /**
     * Registers a new plugin with this reveal.js instance.
     *
     * reveal.js waits for all registered plugins to initialize
     * before considering itself ready, as long as the plugin
     * is registered before calling `Reveal.initialize()`.
     */
    registerPlugin(plugin) {
        // Backwards compatibility to make reveal.js ~3.9.0
        // plugins work with reveal.js 4.0.0
        if (arguments.length === 2 && typeof arguments[0] === "string") {
            plugin = arguments[1];
            plugin.id = arguments[0];
        }
        // Plugin can optionally be a function which we call
        // to create an instance of the plugin
        else if (typeof plugin === "function") {
            plugin = plugin();
        }

        let id = plugin.id;

        if (typeof id !== "string") {
            console.warn(
                "Unrecognized plugin format; can't find plugin.id",
                plugin,
            );
        } else if (this.registeredPlugins[id] === undefined) {
            this.registeredPlugins[id] = plugin;

            // If a plugin is registered after reveal.js is loaded,
            // initialize it right away
            if (this.state === "loaded" && typeof plugin.init === "function") {
                plugin.init(this.Reveal);
            }
        } else {
            console.warn(
                'reveal.js: "' + id + '" plugin has already been registered',
            );
        }
    }

    /**
     * Checks if a specific plugin has been registered.
     *
     * @param {String} id Unique plugin identifier
     */
    hasPlugin(id) {
        return !!this.registeredPlugins[id];
    }

    /**
     * Returns the specific plugin instance, if a plugin
     * with the given ID has been registered.
     *
     * @param {String} id Unique plugin identifier
     */
    getPlugin(id) {
        return this.registeredPlugins[id];
    }

    getRegisteredPlugins() {
        return this.registeredPlugins;
    }

    destroy() {
        Object.values(this.registeredPlugins).forEach((plugin) => {
            if (typeof plugin.destroy === "function") {
                plugin.destroy();
            }
        });

        this.registeredPlugins = {};
        this.asyncDependencies = [];
    }
}
