import { DefaultLoader, isLoaderConstructor } from './DefaultLoader';
import { Scene, isSceneConstructor } from '../Scene';
import { Loader } from './Loader';
import { Logger } from '../Util/Log';
import { ActivateEvent, DeactivateEvent } from '../Events';
import { EventEmitter } from '../EventEmitter';
export const DirectorEvents = {
    NavigationStart: 'navigationstart',
    Navigation: 'navigation',
    NavigationEnd: 'navigationend'
};
/**
 * The Director is responsible for managing scenes and changing scenes in Excalibur.
 *
 * It deals with transitions, scene loaders, switching scenes
 *
 * This is used internally by Excalibur, generally not mean to
 * be instantiated end users directly.
 */
export class Director {
    /**
     * Gets whether the director currently transitioning between scenes
     *
     * Useful if you need to block behavior during transition
     */
    get isTransitioning() {
        return this._isTransitioning;
    }
    constructor(_engine, scenes) {
        this._engine = _engine;
        this.events = new EventEmitter();
        this._logger = Logger.getInstance();
        this._initialized = false;
        /**
         * All registered scenes in Excalibur
         */
        this.scenes = {};
        /**
         * Holds all instantiated scenes
         */
        this._sceneToInstance = new Map();
        this._sceneToLoader = new Map();
        this._sceneToTransition = new Map();
        /**
         * Used to keep track of scenes that have already been loaded so we don't load multiple times
         */
        this._loadedScenes = new Set();
        this._isTransitioning = false;
        this.rootScene = this.currentScene = new Scene();
        this.add('root', this.rootScene);
        this.currentScene = this.rootScene;
        this.currentSceneName = 'root';
        for (const sceneKey in scenes) {
            const sceneOrOptions = scenes[sceneKey];
            this.add(sceneKey, sceneOrOptions);
            if (sceneKey === 'root') {
                this.rootScene = this.getSceneInstance('root');
                this.currentScene = this.rootScene;
            }
        }
    }
    /**
     * Initialize the director's internal state
     */
    async onInitialize() {
        if (!this._initialized) {
            this._initialized = true;
            if (this._deferredGoto) {
                const deferredScene = this._deferredGoto;
                const deferredTransition = this._deferredTransition;
                this._deferredGoto = null;
                this._deferredTransition = null;
                await this.swapScene(deferredScene);
                if (deferredTransition) {
                    await this.playTransition(deferredTransition);
                }
            }
            else {
                await this.swapScene('root');
            }
        }
    }
    get isInitialized() {
        return this._initialized;
    }
    /**
     * Configures the start scene, and optionally the transition & loader for the director
     *
     * Typically this is called at the beginning of the game to the start scene and transition and never again.
     * @param startScene
     * @param options
     */
    configureStart(startScene, options) {
        const maybeLoaderOrCtor = options === null || options === void 0 ? void 0 : options.loader;
        if (maybeLoaderOrCtor instanceof DefaultLoader) {
            this.mainLoader = maybeLoaderOrCtor;
        }
        else if (isLoaderConstructor(maybeLoaderOrCtor)) {
            this.mainLoader = new maybeLoaderOrCtor();
        }
        else {
            this.mainLoader = new Loader();
        }
        let maybeStartTransition;
        if (options) {
            const { inTransition } = options;
            maybeStartTransition = inTransition;
        }
        this.startScene = startScene;
        // Fire and forget promise for the initial scene
        if (maybeStartTransition) {
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            this.swapScene(this.startScene).then(() => {
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                this.playTransition(maybeStartTransition);
            });
        }
        else {
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            this.swapScene(this.startScene);
        }
        this.currentSceneName = this.startScene;
    }
    _getLoader(sceneName) {
        return this._sceneToLoader.get(sceneName);
    }
    _getInTransition(sceneName) {
        var _a;
        const sceneOrRoute = this.scenes[sceneName];
        if (sceneOrRoute instanceof Scene || isSceneConstructor(sceneOrRoute)) {
            return null;
        }
        return (_a = sceneOrRoute === null || sceneOrRoute === void 0 ? void 0 : sceneOrRoute.transitions) === null || _a === void 0 ? void 0 : _a.in;
    }
    _getOutTransition(sceneName) {
        var _a;
        const sceneOrRoute = this.scenes[sceneName];
        if (sceneOrRoute instanceof Scene || isSceneConstructor(sceneOrRoute)) {
            return null;
        }
        return (_a = sceneOrRoute === null || sceneOrRoute === void 0 ? void 0 : sceneOrRoute.transitions) === null || _a === void 0 ? void 0 : _a.out;
    }
    getDeferredScene() {
        const maybeDeferred = this.getSceneDefinition(this._deferredGoto);
        if (this._deferredGoto && maybeDeferred) {
            return maybeDeferred;
        }
        return null;
    }
    /**
     * Returns a scene by name if it exists, might be the constructor and not the instance of a scene
     * @param name
     */
    getSceneDefinition(name) {
        const maybeScene = this.scenes[name];
        if (maybeScene instanceof Scene || isSceneConstructor(maybeScene)) {
            return maybeScene;
        }
        else if (maybeScene) {
            return maybeScene.scene;
        }
        return undefined;
    }
    getSceneName(scene) {
        for (const [name, sceneInstance] of this._sceneToInstance) {
            if (scene === sceneInstance) {
                return name;
            }
        }
        return 'unknown scene name';
    }
    /**
     * Returns the same Director, but asserts a scene DOES exist to the type system
     * @param name
     */
    assertAdded(name) {
        return this;
    }
    /**
     * Returns the same Director, but asserts a scene DOES NOT exist to the type system
     * @param name
     */
    assertRemoved(name) {
        return this;
    }
    /**
     * Adds additional Scenes to the game!
     * @param name
     * @param sceneOrRoute
     */
    add(name, sceneOrRoute) {
        if (!(sceneOrRoute instanceof Scene) && !isSceneConstructor(sceneOrRoute)) {
            const { loader, transitions } = sceneOrRoute;
            const { in: inTransition, out: outTransition } = transitions !== null && transitions !== void 0 ? transitions : {};
            this._sceneToTransition.set(name, { in: inTransition, out: outTransition });
            if (isLoaderConstructor(loader)) {
                this._sceneToLoader.set(name, new loader());
            }
            else {
                this._sceneToLoader.set(name, loader);
            }
        }
        if (this.scenes[name]) {
            this._logger.warn('Scene', name, 'already exists overwriting');
        }
        this.scenes[name] = sceneOrRoute;
        return this.assertAdded(name);
    }
    remove(nameOrScene) {
        if (nameOrScene instanceof Scene || isSceneConstructor(nameOrScene)) {
            const sceneOrCtor = nameOrScene;
            // remove scene
            for (const key in this.scenes) {
                if (this.scenes.hasOwnProperty(key)) {
                    const potentialSceneOrOptions = this.scenes[key];
                    let scene;
                    if (potentialSceneOrOptions instanceof Scene || isSceneConstructor(potentialSceneOrOptions)) {
                        scene = potentialSceneOrOptions;
                    }
                    else {
                        scene = potentialSceneOrOptions.scene;
                    }
                    if (scene === sceneOrCtor) {
                        if (key === this.currentSceneName) {
                            throw new Error(`Cannot remove a currently active scene: ${key}`);
                        }
                        this._sceneToInstance.delete(key);
                        this._sceneToTransition.delete(key);
                        this._sceneToLoader.delete(key);
                        delete this.scenes[key];
                    }
                }
            }
        }
        if (typeof nameOrScene === 'string') {
            if (nameOrScene === this.currentSceneName) {
                throw new Error(`Cannot remove a currently active scene: ${nameOrScene}`);
            }
            // remove scene
            this._sceneToInstance.delete(nameOrScene);
            this._sceneToTransition.delete(nameOrScene);
            this._sceneToLoader.delete(nameOrScene);
            delete this.scenes[nameOrScene];
        }
    }
    /**
     * Go to a specific scene, and optionally override loaders and transitions
     * @param destinationScene
     * @param options
     */
    async goto(destinationScene, options) {
        var _a, _b, _c, _d, _e, _f;
        const maybeDest = this.getSceneInstance(destinationScene);
        if (!maybeDest) {
            this._logger.warn(`Scene ${destinationScene} does not exist! Check the name, are you sure you added it?`);
            return;
        }
        const sourceScene = this.currentSceneName;
        const engineInputEnabled = (_b = (_a = this._engine.input) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
        this._isTransitioning = true;
        const maybeSourceOut = (_c = this.getSceneInstance(sourceScene)) === null || _c === void 0 ? void 0 : _c.onTransition('out');
        const maybeDestinationIn = maybeDest === null || maybeDest === void 0 ? void 0 : maybeDest.onTransition('in');
        options = {
            // Engine configuration then dynamic scene transitions
            ...{ sourceOut: (_d = this._getOutTransition(this.currentSceneName)) !== null && _d !== void 0 ? _d : maybeSourceOut },
            ...{ destinationIn: (_e = this._getInTransition(destinationScene)) !== null && _e !== void 0 ? _e : maybeDestinationIn },
            // Goto options
            ...options
        };
        const { sourceOut, destinationIn, sceneActivationData } = options;
        const outTransition = sourceOut !== null && sourceOut !== void 0 ? sourceOut : this._getOutTransition(this.currentSceneName);
        const inTransition = destinationIn !== null && destinationIn !== void 0 ? destinationIn : this._getInTransition(destinationScene);
        const hideLoader = (outTransition === null || outTransition === void 0 ? void 0 : outTransition.hideLoader) || (inTransition === null || inTransition === void 0 ? void 0 : inTransition.hideLoader);
        if (hideLoader) {
            // Start hidden loader early and take advantage of the transition
            // Don't await and block on a hidden loader
            // eslint-disable-next-line @typescript-eslint/no-floating-promises
            this.maybeLoadScene(destinationScene, hideLoader);
        }
        this._emitEvent('navigationstart', sourceScene, destinationScene);
        // Run the out transition on the current scene if present
        await this.playTransition(outTransition);
        // Run the loader if present
        await this.maybeLoadScene(destinationScene, hideLoader);
        // Give incoming transition a chance to grab info from previous
        await (inTransition === null || inTransition === void 0 ? void 0 : inTransition.onPreviousSceneDeactivate(this.currentScene));
        // Swap to the new scene
        await this.swapScene(destinationScene, sceneActivationData);
        this._emitEvent('navigation', sourceScene, destinationScene);
        // Run the in transition on the new scene if present
        await this.playTransition(inTransition);
        this._emitEvent('navigationend', sourceScene, destinationScene);
        (_f = this._engine.input) === null || _f === void 0 ? void 0 : _f.toggleEnabled(engineInputEnabled);
        this._isTransitioning = false;
    }
    /**
     * Retrieves a scene instance by key if it's registered.
     *
     * This will call any constructors that were given as a definition
     * @param scene
     */
    getSceneInstance(scene) {
        const sceneDefinition = this.getSceneDefinition(scene);
        if (!sceneDefinition) {
            return undefined;
        }
        if (this._sceneToInstance.has(scene)) {
            return this._sceneToInstance.get(scene);
        }
        if (sceneDefinition instanceof Scene) {
            this._sceneToInstance.set(scene, sceneDefinition);
            return sceneDefinition;
        }
        const newScene = new sceneDefinition();
        this._sceneToInstance.set(scene, newScene);
        return newScene;
    }
    /**
     * Triggers scene loading if has not already been loaded
     * @param scene
     * @param hideLoader
     */
    async maybeLoadScene(scene, hideLoader = false) {
        var _a;
        const loader = (_a = this._getLoader(scene)) !== null && _a !== void 0 ? _a : new DefaultLoader();
        const sceneToLoad = this.getSceneDefinition(scene);
        const sceneToLoadInstance = this.getSceneInstance(scene);
        if (sceneToLoad && sceneToLoadInstance && !this._loadedScenes.has(sceneToLoadInstance)) {
            sceneToLoadInstance.onPreLoad(loader);
            sceneToLoadInstance.events.emit('preload', { loader });
            if (hideLoader) {
                // Don't await a hidden loader
                // eslint-disable-next-line @typescript-eslint/no-floating-promises
                this._engine.load(loader, hideLoader);
            }
            else {
                await this._engine.load(loader);
            }
            this._loadedScenes.add(sceneToLoadInstance);
        }
    }
    /**
     * Plays a transition in the current scene
     * @param transition
     */
    async playTransition(transition) {
        var _a, _b, _c, _d, _e, _f, _g;
        if (!this.isInitialized) {
            this._deferredTransition = transition;
            return;
        }
        if (transition) {
            this.currentTransition = transition;
            const currentScene = this._engine.currentScene;
            const sceneInputEnabled = (_b = (_a = currentScene.input) === null || _a === void 0 ? void 0 : _a.enabled) !== null && _b !== void 0 ? _b : true;
            (_c = currentScene.input) === null || _c === void 0 ? void 0 : _c.toggleEnabled(!transition.blockInput);
            (_d = this._engine.input) === null || _d === void 0 ? void 0 : _d.toggleEnabled(!transition.blockInput);
            await this.currentTransition.play(this._engine);
            (_e = currentScene.input) === null || _e === void 0 ? void 0 : _e.toggleEnabled(sceneInputEnabled);
        }
        (_f = this.currentTransition) === null || _f === void 0 ? void 0 : _f.kill();
        (_g = this.currentTransition) === null || _g === void 0 ? void 0 : _g.reset();
        this.currentTransition = null;
    }
    /**
     * Swaps the current and destination scene after performing required lifecycle events
     * @param destinationScene
     * @param data
     */
    async swapScene(destinationScene, data) {
        const engine = this._engine;
        // if not yet initialized defer goToScene
        if (!this.isInitialized) {
            this._deferredGoto = destinationScene;
            return;
        }
        const maybeDest = this.getSceneInstance(destinationScene);
        if (maybeDest) {
            const previousScene = this.currentScene;
            const nextScene = maybeDest;
            this._logger.debug('Going to scene:', destinationScene);
            // only deactivate when initialized
            if (this.currentScene.isInitialized) {
                const context = { engine, previousScene, nextScene };
                await this.currentScene._deactivate(context);
                this.currentScene.events.emit('deactivate', new DeactivateEvent(context, this.currentScene));
            }
            // wait for the scene to be loaded if needed
            const destLoader = this._sceneToLoader.get(destinationScene);
            await (destLoader === null || destLoader === void 0 ? void 0 : destLoader.areResourcesLoaded());
            // set current scene to new one
            this.currentScene = nextScene;
            this.currentSceneName = destinationScene;
            engine.screen.setCurrentCamera(nextScene.camera);
            // initialize the current scene if has not been already
            await this.currentScene._initialize(engine);
            const context = { engine, previousScene, nextScene, data };
            await this.currentScene._activate(context);
            this.currentScene.events.emit('activate', new ActivateEvent(context, this.currentScene));
        }
        else {
            this._logger.error('Scene', destinationScene, 'does not exist!');
        }
    }
    _emitEvent(eventName, sourceScene, destinationScene) {
        const source = this.getSceneDefinition(sourceScene);
        const dest = this.getSceneDefinition(destinationScene);
        this.events.emit(eventName, {
            sourceScene: source,
            sourceName: sourceScene,
            destinationScene: dest,
            destinationName: destinationScene
        });
    }
}
//# sourceMappingURL=Director.js.map