import { BaseProcess } from "../../core/process/BaseProcess.js";
import { ProcessState } from "../../core/process/ProcessState.js";
import { EnginePlugin } from "./EnginePlugin.js";
import { PluginReferenceContext } from "./PluginReferenceContext.js";
import { isSubclassOf } from "./isSubclassOf.js";
import { assert } from "../../core/assert.js";

export class EnginePluginManager extends BaseProcess {

    /**
     * @private
     * @type {Map<Class, PluginReferenceContext>}
     */
    __plugins = new Map();

    /**
     *
     * @type {Engine}
     */
    engine = null;

    /**
     *
     * @type {number}
     * @private
     */
    __version = 0;

    /**
     *
     * @returns {EnginePlugin[]}
     * @private
     */
    __getActivePluginInstances() {
        const result = [];

        for (const [PluginClass, ctx] of this.__plugins) {
            if (ctx.referenceCount <= 0) {
                continue;
            }

            result.push(ctx.instance);
        }

        return result;
    }

    /**
     *
     * @returns {PluginReferenceContext[]}
     * @private
     */
    __getActivePluginContexts() {
        const result = [];

        for (const [PluginClass, ctx] of this.__plugins) {
            if (ctx.referenceCount <= 0) {
                continue;
            }

            result.push(ctx);
        }

        return result;
    }


    /**
     *
     * @param {PluginReferenceContext} context
     * @param {ProcessState} state
     * @returns {Promise<void>}
     * @private
     */
    async __transition_plugin_to_state(context, state) {
        const plugin = context.instance;

        if (state === ProcessState.Initialized || state === ProcessState.Running) {
            const dependencies = plugin.dependencies;

            // resolve dependencies
            for (let i = 0; i < dependencies.length; i++) {
                const dependency = dependencies[i];

                const dependency_context = this.__plugins.get(dependency);

                const dependency_state = dependency_context.instance.getState().getValue();

                if (dependency_state !== ProcessState.Running) {
                    if (state === ProcessState.Running) {
                        await dependency_context.transition(state, this.engine);
                    } else if (dependency_state !== ProcessState.Initialized) {
                        // initialized state
                        await dependency_context.transition(state, this.engine);
                    }
                }
            }
        }

        await context.transition(state, this.engine);
    }

    /**
     *
     * @param {ProcessState} state
     * @returns {Promise<void>}
     * @private
     */
    async __transition_active_plugins_to(state) {
        let v0;
        do {
            v0 = this.__version;

            /**
             *
             * @type {PluginReferenceContext[]}
             */
            const contexts = this.__getActivePluginContexts();

            const context_count = contexts.length;

            const promises = [];

            for (let i = 0; i < context_count; i++) {
                const context = contexts[i];

                const promise = this.__transition_plugin_to_state(context, state);

                promises.push(promise);
            }

            await Promise.all(promises);

        } while (v0 !== this.__version);

    }

    /**
     *
     * @param {PluginReferenceContext} ctx
     * @private
     */
    __handle_last_plugin_reference_release(ctx) {
        this.__version++;

        // remove plugin reference
        const PluginClass = ctx.getKlass();

        const removed = this.__plugins.delete(PluginClass);

        if (!removed) {
            return;
        }

        ctx.dispose();
    }

    /**
     *
     * @param {Engine} engine
     */
    initialize(engine) {
        this.engine = engine;

        super.initialize();

        const enginePlugins = this.__getActivePluginInstances();
        const n = enginePlugins.length;


        for (let i = 0; i < n; i++) {
            const plugin = enginePlugins[i];

            plugin.initialize(engine);

        }

    }

    finalize() {
        super.finalize()

        const enginePlugins = this.__getActivePluginInstances();
        const n = enginePlugins.length;


        for (let i = 0; i < n; i++) {
            const plugin = enginePlugins[i];

            plugin.finalize();

        }

    }

    async startup() {
        super.startup();

        return this.__transition_active_plugins_to(ProcessState.Running);
    }

    async shutdown() {
        super.shutdown();

        return this.__transition_active_plugins_to(ProcessState.Finalized);
    }

    /**
     * @template T
     * @param {Class<T>} PluginClass
     * @returns {Promise<Reference<T>>}
     */
    async acquire(PluginClass) {
        // check plugin class hierarchy
        if (!isSubclassOf(PluginClass, EnginePlugin)) {
            throw new Error(`PluginClass is not a subclass of EnginePlugin`);
        }

        let ctx = this.__plugins.get(PluginClass);
        let reference;

        if (ctx === undefined) {

            this.__version++;

            // context not found, create it

            const instance = new PluginClass();

            ctx = new PluginReferenceContext(instance, PluginClass);

            // monitor the context
            ctx.on.lastReleased.add(this.__handle_last_plugin_reference_release, this);

            // all dependencies acquired, register the plugin
            this.__plugins.set(PluginClass, ctx);

            // get reference to make sure that the context doesn't come up as having no references
            reference = ctx.getReference();

            // TODO address cyclic dependencies, these can cause a deadlock
            // acquire dependencies
            const dependency_refs = await this.acquireMany(instance.dependencies);

            ctx.dependency_references.addAll(dependency_refs);


            const manager_state_object = this.getState();


            const manager_state_value = manager_state_object.getValue();

            // bring the plugin into proper state

            const engine = this.engine;

            await ctx.transition(manager_state_value, engine);

        } else {
            reference = ctx.getReference();

            await ctx.synchronize();
        }


        return reference;

    }

    /**
     *
     * @param {Class[]} PluginClasses
     * @returns {Promise<Reference[]>}
     */
    async acquireMany(PluginClasses) {
        assert.isArray(PluginClasses, 'PluginClasses');

        const promises = PluginClasses.map(this.acquire, this);

        return await Promise.all(promises);
    }

    /**
     * @template T
     * @param {Class<T>} PluginClass
     * @returns {T|undefined}
     */
    getPlugin(PluginClass) {

        const plugins = this.__getActivePluginInstances();
        const n = plugins.length;

        for (let i = 0; i < n; i++) {
            const enginePlugin = plugins[i];

            if (enginePlugin.constructor === PluginClass) {
                return enginePlugin;
            }
        }

    }

    /**
     * @template T
     * @param {string} id
     * @returns {EnginePlugin|T|undefined}
     */
    getById(id) {
        const plugins = this.__getActivePluginInstances();
        const n = plugins.length;
        for (let i = 0; i < n; i++) {
            const enginePlugin = plugins[i];

            if (enginePlugin.id === id) {
                return enginePlugin;
            }
        }
    }
}
