import List from "../../core/collection/list/List.js";
import { EntityManager } from "../ecs/EntityManager.js";
import Scene from "./Scene.js";


class SceneManager {
    /**
     *
     * @type {Scene|null}
     */
    #current_scene = null

    /**
     *
     * @return {Scene|null}
     */
    get current_scene() {
        return this.#current_scene;
    }

    /**
     *
     * @type {EntityManager|null}
     */
    #entity_manager = null;

    /**
     *
     * @return {EntityManager|null}
     */
    get entity_manager(){
        return this.#entity_manager;
    }

    /**
     *
     * @param {EntityManager} entityManager
     * @param {Clock} clock
     * @constructor
     */
    constructor(entityManager, clock) {

        /**
         * @type {List<Scene>}
         */
        this.scenes = new List();

        /**
         *
         * @type {EntityManager}
         */
        this.#entity_manager = entityManager;

        /**
         *
         * @type {Clock}
         */
        this.clock = clock;

        /**
         * Used to track scene transitions
         * @readonly
         * @private
         * @type {string[]}
         */
        this.stack = [];
    }

    /**
     *
     * @param {string} name
     * @returns {Scene}
     */
    create(name) {
        const scene = new Scene(name);

        this.add(scene);

        return scene;
    }

    /**
     *
     * @param {Scene} scene
     */
    add(scene) {
        if (this.exists(scene.name)) {
            throw new Error(`Scene named '${scene.name}' already exists`);
        }

        this.scenes.add(scene);
    }

    /**
     *
     * @param {string} name
     * @returns {number}
     */
    indexByName(name) {
        const length = this.scenes.length;
        for (let i = 0; i < length; i++) {
            const scene = this.scenes.get(i);

            if (scene.name === name) {
                return i;
            }
        }

        return -1;
    }

    /**
     * @template T
     * @param {string} name
     * @returns {Scene|T|undefined}
     */
    getByName(name) {
        return this.scenes.find(function (scene) {
            return scene.name === name;
        });
    }

    /**
     *
     * @param {string} name
     * @returns {boolean}
     */
    remove(name) {
        const sceneIndex = this.indexByName(name);
        if (sceneIndex === -1) {
            //doesn't exist, no need to delete
            return false;
        }

        const scene = this.scenes.get(sceneIndex);


        if (this.#current_scene === scene) {
            this.deactivateCurrentScene();

            this.#current_scene = null;
        }

        this.scenes.remove(sceneIndex);

        return true;
    }

    /**
     *
     * @returns {SceneManager}
     */
    clear() {

        if (this.#current_scene !== null) {
            this.#current_scene.active.set(false);
            this.#entity_manager.detachDataset();

            this.#current_scene = null;
        }

        return this;
    }

    /**
     *
     * @param {string} name
     * @returns {boolean}
     */
    exists(name) {
        return this.getByName(name) !== undefined;
    }

    /**
     * @private
     * @param {Scene} scene
     */
    deactivateScene(scene) {

        if (!scene.active.getValue()) {
            //not active, nothing to do
            return;
        }

        try {
            scene.handlePreDeactivation();
        } catch (e) {
            console.error(`Exception in pre-deactivation routine of scene '${scene.name}'`, e);
        }

        scene.active.set(false);

        if (this.#entity_manager.dataset === scene.dataset) {
            this.#entity_manager.detachDataset();
        }

        //remove speed modifiers
        scene.speedModifiers.forEach(m => this.clock.speed.removeModifier(m));

        //unwatch modifiers
        scene.speedModifiers.on.added.remove(this.__handleSpeedModifierAdded, this);
        scene.speedModifiers.on.removed.remove(this.__handleSpeedModifierRemoved, this);

        try {
            scene.handlePostDeactivation();
        } catch (e) {
            console.error(`Exception in post-deactivation routine of scene '${scene.id}'`, e);
        }
    }

    /**
     * @private
     * @param {Scene} scene
     */
    activateScene(scene) {

        try {
            scene.handlePreActivation();
        } catch (e) {
            console.error(`Exception in pre-activation routine of scene '${scene.id}'`, e);
        }

        const em = this.#entity_manager;

        em.attachDataset(scene.dataset);

        scene.active.set(true);

        scene.speedModifiers.forEach(m => this.clock.speed.addModifier(m));
        //watch speed modifiers
        scene.speedModifiers.on.added.add(this.__handleSpeedModifierAdded, this);
        scene.speedModifiers.on.removed.add(this.__handleSpeedModifierRemoved, this);


        try {
            scene.handlePostActivation();
        } catch (e) {
            console.error(`Exception in post-activation routine of scene '${scene.id}'`, e);
        }

        this.#current_scene = scene;
    }

    /**
     * @private
     */
    deactivateCurrentScene() {

        const currentScene = this.#current_scene;

        if (currentScene !== null) {
            this.deactivateScene(currentScene);
        }

    }

    /**
     * @private
     * @param {LinearModifier} mod
     */
    __handleSpeedModifierAdded(mod) {
        this.clock.speed.addModifier(mod);
    }

    /**
     * @private
     * @param {LinearModifier} mod
     */
    __handleSpeedModifierRemoved(mod) {
        this.clock.speed.removeModifier(mod);
    }

    /**
     *
     * @param {string} name
     */
    set(name) {
        const scene = this.getByName(name);

        if (scene === undefined) {
            throw new Error(`Scene named '${name}' doesn't exist, valid options are: [${this.scenes.map(s => s.name).join(', ')}]`);
        }

        if (this.#current_scene === scene) {
            //already at that scene
            return;
        }

        const em = this.#entity_manager;

        // Ensure that all system components are registered
        scene.dataset.registerManyComponentTypes(em.getComponentTypeMap());

        this.deactivateCurrentScene();
        this.activateScene(scene);
    }

    /**
     *
     * @param {string} id
     */
    stackPush(id) {
        //take current scene and put it onto the stack
        this.stack.push(this.#current_scene.name);

        this.set(id);
    }

    /**
     *
     * @returns {string} ID of the popped scene
     */
    stackPop() {
        const id = this.stack.pop();

        this.set(id);

        return id;
    }

    /**
     * Clear out current stack of scenes
     */
    stackDrop() {
        this.stack.splice(0, this.stack.length);
    }

    update(timeDelta) {

    }
}


export default SceneManager;
