var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { EquirectangularReflectionMapping, Object3D, Scene } from "three";
import { AssetReference } from "../engine/engine_addressables.js";
import { destroy, instantiate } from "../engine/engine_gameobject.js";
import { InputEvents } from "../engine/engine_input.js";
import { isLocalNetwork } from "../engine/engine_networking_utils.js";
import { serializable } from "../engine/engine_serialization.js";
import { getParam, setParamWithoutReload } from "../engine/engine_utils.js";
import { registerObservableAttribute } from "../engine/webcomponents/needle-engine.extras.js";
import { Behaviour, GameObject } from "./Component.js";
import { EventList } from "./EventList.js";
const debug = getParam("debugsceneswitcher");
const experimental_clearSceneOnLoad = getParam("sceneswitcher:clearscene");
const ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME = "scene";
registerObservableAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
// ContextRegistry.registerCallback(ContextEvent.ContextRegistered, async _ => {
//     // We need to defer import to not get issues with circular dependencies
//     import("../engine/engine_element").then(res => {
//         const webcomponent = res.NeedleEngineHTMLElement;
//         if (debug) console.log("SceneSwitcher: registering scene attribute", webcomponent.observedAttributes);
//         if (!webcomponent.observedAttributes.includes(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME))
//             webcomponent.observedAttributes.push(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
//     });
// });
const couldNotLoadScenePromise = Promise.resolve(false);
/** The SceneSwitcher can be used to dynamically load and unload extra content
 * Available scenes are defined in the `scenes` array.
 * Loaded scenes will be added to the SceneSwitcher's GameObject as a child and removed when another scene is loaded by the same SceneSwitcher.
 * Live Examples
 * - [Multi Scenes Sample](https://engine.needle.tools/samples/multi-scene-example) (source code available)
 * - [Needle Website](https://needle.tools)
 * - [Songs Of Cultures](https://app.songsofcultures.com)
 *
 * ![](https://cloud.needle.tools/-/media/I-atrBcwIcg2kfvPl8c71A.gif)
 * *Replace entire scenes with the SceneSwitcher.*
 *
 * ![](https://cloud.needle.tools/-/media/_FbH-7pkDmluTdfphXlP-A.gif)
 * *Multiple SceneSwitcher components can be used at the same time and they can also be nested
 * (a scene loaded by a SceneSwitcher can also have a SceneSwitcher to load sub-scenes).*
 *
 * ### Interfaces
 * Use the {@link ISceneEventListener} interface to listen to scene open and closing events with the ability to modify transitions and stall the scene loading process.
 *
 * ### Events
 * - `loadscene-start`: Called when a scene starts loading
 * - `loadscene-finished`: Called when a scene finished loading
 * - `progress`: Called when a scene is loading and the progress changes
 * - `scene-opened`: Called when a scene is loaded and added to the SceneSwitcher's GameObject
 * @example
 * ```ts
 * sceneSwitcher.addEventListener("loadscene-start", (e) => {
 *    console.log("Loading scene", e.detail.scene.url);
 * });
 * sceneSwitcher.addEventListener("loadscene-finished", (e) => {
 *   console.log("Finished loading scene", e.detail.scene.url);
 * });
 * sceneSwitcher.addEventListener("progress", (e) => {
 *  console.log("Loading progress", e.loaded, e.total);
 * });
 * sceneSwitcher.addEventListener("scene-opened", (e) => {
 * console.log("Scene opened", e.detail.scene.url);
 * });
 * ```
 *
 * @summary Dynamically loads and switches between multiple scenes
 * @category Asset Management
 * @group Components
 * @see {@link ISceneEventListener} for scene transition callbacks
 * @see {@link AssetReference} for loading individual assets
 * @see {@link NestedGltf} for embedding static glTF content
 * @link https://engine.needle.tools/docs/how-to-guides/components/scene-switcher.html
 */
export class SceneSwitcher extends Behaviour {
    /** When enabled the first scene will be loaded when the SceneSwitcher becomes active
     * @default true
    */
    autoLoadFirstScene = true;
    /**
     * The scenes that can be loaded by the SceneSwitcher.
     * @default []
     */
    scenes = [];
    /**
     * The scene that is displayed while a scene is loading.
     * @default undefined
     */
    loadingScene;
    /** the url parameter that is set/used to store the currently loaded scene in, set to "" to disable
     * @default "scene"
    */
    queryParameterName = "scene";
    /**
     * when enabled the scene name will be used as the query parameter (otherwise the scene index will be used)
     * Needs `queryParameterName` set
     * @default true
     */
    useSceneName = true;
    /**
     * When enabled the current scene index will be clamped to the scenes array bounds.
     * For example when the last scene is loaded and `clamp` is true then trying to load the `next()` scene will not change the scene.
     * When `clamp` is false and the last scene is loaded then the first scene will be loaded instead.
     * @default true
     */
    clamp = true;
    /** when enabled the new scene is pushed to the browser navigation history, only works with a valid query parameter set
     * @default true
    */
    useHistory = true;
    /** when enabled you can switch between scenes using keyboard left, right, A and D or number keys
     * @default true
    */
    useKeyboard = true;
    /** when enabled you can switch between scenes using swipe (mobile only)
     * @default true
    */
    useSwipe = true;
    /** when enabled will automatically apply the environment scene lights
     * @default true
    */
    useSceneLighting = true;
    /** When enabled will automatically apply the skybox from the loaded scene
     * @default true
     */
    useSceneBackground = true;
    /** how many scenes after the currently active scene should be preloaded
     * @default 1
    */
    preloadNext = 1;
    /** how many scenes before the currently active scene should be preloaded
     * @default 1
    */
    preloadPrevious = 1;
    /** how many scenes can be loaded in parallel
     * @default 2
    */
    preloadConcurrent = 2;
    /**
     * When enabled will create a button for the Needle menu to switch to the next or previous scene
     * @default false
     */
    createMenuButtons = false;
    /** The index of the currently loaded and active scene */
    get currentIndex() { return this._currentIndex; }
    /** Get the progress of the currently loading scene. This is undefined if no scene is loading
     * You can also subscribe to the loading event by adding an event listener to the scene switcher.
     * For example like this `sceneSwitcher.addEventListeneer("progress", (e) => {...})`
     */
    get currentLoadingProgress() { return this._currentLoadingProgress; }
    /** The currently loading scene. This is undefined if no scene is loading. */
    get currentlyLoadingScene() { return this._currentlyLoadingScene; }
    /**
     * The currently loaded scene. This is undefined if no scene is loaded.
     */
    get currentlyLoadedScene() { return this._currentScene; }
    /**
     * Called when a scene starts loading
     */
    sceneLoadingStart = new EventList();
    sceneLoadingProgress = new EventList();
    /**
     * The sceneLoaded event is called when a scene/glTF is loaded and added to the scene
     */
    sceneLoaded = new EventList();
    _currentIndex = -1;
    _currentScene = undefined;
    _currentSceneAsset = undefined;
    _engineElementOverserver = undefined;
    _preloadScheduler;
    _menuButtons;
    // this is the scene that was requested last
    __lastSwitchScene;
    __lastSwitchScenePromise;
    /** @internal */
    awake() {
        this._currentScene = undefined;
        this._lastLoadingScene = undefined;
        this.__lastSwitchScenePromise = undefined;
        if (this.scenes === undefined)
            this.scenes = [];
        // remove all scenes from the url that are in the scenes array at startup
        for (const scene of this.scenes) {
            if (scene && !scene.hasUrl && scene.asset instanceof Object3D) {
                GameObject.remove(scene.asset);
            }
            else if (scene instanceof Object3D) {
                GameObject.remove(scene);
            }
        }
        if (debug)
            console.log("SceneSwitcher", this);
    }
    /** @internal */
    async onEnable() {
        globalThis.addEventListener("popstate", this.onPopState);
        this.context.input.addEventListener(InputEvents.KeyDown, this.onInputKeyDown);
        this.context.input.addEventListener(InputEvents.PointerMove, this.onInputPointerMove);
        this.context.input.addEventListener(InputEvents.PointerUp, this.onInputPointerUp);
        if (!this._engineElementOverserver) {
            this._engineElementOverserver = new MutationObserver((mutations) => {
                for (const mut of mutations) {
                    if (mut.type === "attributes" && mut.attributeName === ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME) {
                        const value = this.context.domElement.getAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
                        if (value !== null) {
                            this.trySelectSceneFromValue(value);
                        }
                    }
                }
            });
        }
        this._engineElementOverserver.observe(this.context.domElement, {
            attributes: true
        });
        if (!this._preloadScheduler)
            this._preloadScheduler = new PreLoadScheduler(this);
        this._preloadScheduler.maxLoadAhead = this.preloadNext;
        this._preloadScheduler.maxLoadBehind = this.preloadPrevious;
        this._preloadScheduler.maxConcurrent = this.preloadConcurrent;
        this._preloadScheduler.begin(2000);
        // Begin loading the loading scene
        if (this.autoLoadFirstScene && this._currentIndex === -1 && !await this.tryLoadFromQueryParam()) {
            const value = this.context.domElement.getAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
            // let locked = this.lock;
            try {
                // this.lock = false;
                if (value === null || !await this.trySelectSceneFromValue(value)) {
                    if (this._currentIndex === -1)
                        this.select(0);
                }
            }
            finally {
                // this.lock = locked;
            }
        }
        // create the menu buttons
        if (this.createMenuButtons) {
            this._menuButtons ??= [];
            this._menuButtons.push(this.context.menu.appendChild({
                label: "Previous",
                icon: "arrow_back_ios",
                onClick: () => this.selectPrev(),
                priority: -1005,
                class: "row2"
            }));
            this._menuButtons.push(this.context.menu.appendChild({
                label: "Next",
                icon: "arrow_forward_ios",
                iconSide: "right",
                onClick: () => this.selectNext(),
                priority: -1000,
                class: "row2"
            }));
        }
    }
    /** @internal */
    onDisable() {
        globalThis.removeEventListener("popstate", this.onPopState);
        this.context.input.removeEventListener(InputEvents.KeyDown, this.onInputKeyDown);
        this.context.input.removeEventListener(InputEvents.PointerMove, this.onInputPointerMove);
        this.context.input.removeEventListener(InputEvents.PointerUp, this.onInputPointerUp);
        this._preloadScheduler?.stop();
        // remove the menu buttons
        if (this._menuButtons) {
            for (const button of this._menuButtons) {
                button.remove();
            }
            this._menuButtons = undefined;
        }
    }
    onPopState = async (_state) => {
        if (!this.useHistory)
            return;
        const wasUsingHistory = this.useHistory;
        try {
            this.useHistory = false;
            let didResolve = false;
            if (this.queryParameterName)
                didResolve = await this.tryLoadFromQueryParam();
            if (!didResolve) {
                const state = _state?.state;
                if (state && state.startsWith(this.guid)) {
                    const value = state.substr(this.guid.length + 2);
                    if (debug)
                        console.log("PopState", value);
                    await this.trySelectSceneFromValue(value);
                }
            }
        }
        finally {
            this.useHistory = wasUsingHistory;
        }
    };
    normalizedSwipeThresholdX = 0.1;
    _didSwipe = false;
    onInputPointerMove = (e) => {
        if (!this.useSwipe)
            return;
        if (!this._didSwipe && e.button === 0 && e.pointerType === "touch" && this.context.input.getPointerPressedCount() === 1) {
            const delta = this.context.input.getPointerPositionDelta(e.button);
            if (delta) {
                const normalizedX = delta.x / this.context.domWidth;
                if (normalizedX >= this.normalizedSwipeThresholdX) {
                    this._didSwipe = true;
                    this.selectPrev();
                }
                else if (normalizedX <= -this.normalizedSwipeThresholdX) {
                    this._didSwipe = true;
                    this.selectNext();
                }
            }
        }
    };
    onInputPointerUp = (e) => {
        if (e.button === 0) {
            this._didSwipe = false;
        }
    };
    onInputKeyDown = (e) => {
        if (!this.useKeyboard)
            return;
        if (!this.scenes)
            return;
        const key = e.key.toLowerCase();
        if (!key)
            return;
        const index = parseInt(key) - 1;
        if (index >= 0) {
            this.trySelectSceneFromValue(index);
            return;
        }
        switch (key) {
            case "arrowright":
            case "d":
                this.selectNext();
                break;
            case "arrowleft":
            case "a":
                this.selectPrev();
                break;
        }
    };
    /**
     * Add a scene to the SceneSwitcher.
     * If the scene is already added it will be added again.
     * @param urlOrAssetReference The url of the scene or an AssetReference to the scene
     * @returns The AssetReference of the scene that was added
     * @example
     * ```ts
     * // adding a scene:
     * sceneSwitcher.addScene("scene1.glb");
     * // add another scene and load it:
     * const scene2 = sceneSwitcher.addScene("scene2.glb");
     * sceneSwitcher.switchScene(scene2).then(res => { console.log("Scene loaded", res); });
     * ```
     */
    addScene(urlOrAssetReference) {
        if (typeof urlOrAssetReference === "string") {
            let assetReference = this.context.addressables.findAssetReference(urlOrAssetReference);
            if (!assetReference) {
                assetReference = new AssetReference(urlOrAssetReference);
                this.context.addressables.registerAssetReference(assetReference);
            }
            this.scenes.push(assetReference);
            return assetReference;
        }
        this.scenes.push(urlOrAssetReference);
        return urlOrAssetReference;
    }
    /**
     * Load the next scene in the scenes array ({@link this.currentIndex} + 1)
     * If the current scene is the last scene in the array and {@link this.clamp} is disabled then the first scene will be loaded.
     * @returns a promise that resolves to true if the scene was loaded successfully
     */
    selectNext() {
        return this.select(this._currentIndex + 1);
    }
    /**
     * Load the previous scene in the scenes array ({@link this.currentIndex} - 1)
     * If the current scene is the first scene in the array and {@link this.clamp} is disabled then the last scene will be loaded.
     * @returns a promise that resolves to true if the scene was loaded successfully
     */
    selectPrev() {
        return this.select(this._currentIndex - 1);
    }
    /**
     * Load a scene by its index in the scenes array.
     * @param index The index of the scene or a string that represents the scene uri (if the url is not known to the SceneSwitcher it will try to load the scene by its uri but it won't be added to the current scenes array. Use {@link addScene} to add a scene to the SceneSwitcher)
     * @returns a promise that resolves to true if the scene was loaded successfully
     */
    select(index) {
        if (debug)
            console.log("[SceneSwitcher] select", index);
        if (typeof index === "object") {
            // If a user tries to reference a scene object in a UnityEvent and invoke select(obj) 
            // Then the object will be serialized as a object { guid : ... } or with the index json pointer
            // This case is not supported right now. Object references in the editor must not be scene references
            console.warn("[SceneSwitcher] Switching to \"" + index + "\" might not work. Please either use an index or a AssetReference (not a scene reference)");
        }
        if (typeof index === "string") {
            // If the parameter is a string we try to resolve the scene by its uri
            // it's either already known in the scenes array
            // or we create/get a new AssetReference and try to switch to that
            const scene = this.scenes?.find(s => s.url === index);
            if (!scene) {
                // Ok the scene is unknown to the scene switcher
                // we create a new asset reference (or get an existing one)
                // And switch to that. With this we can not modify the history
                // Until the scene switcher can store the uri in the history instead of the index
                const reference = AssetReference.getOrCreate(this.sourceId ?? "", index, this.context);
                return this.switchScene(reference);
            }
            if (scene)
                index = this.scenes?.indexOf(scene);
            else
                return couldNotLoadScenePromise;
        }
        if (!this.scenes?.length)
            return couldNotLoadScenePromise;
        if (index < 0) {
            if (this.clamp)
                return couldNotLoadScenePromise;
            index = this.scenes.length - 1;
        }
        else if (index >= this.scenes.length) {
            if (this.clamp)
                return couldNotLoadScenePromise;
            index = 0;
        }
        const scene = this.scenes[index];
        return this.switchScene(scene);
    }
    /**
     * Unload the currently loaded scene.
     */
    unload() {
        this.__lastSwitchScene = undefined;
        this.__lastSwitchScenePromise = undefined;
        return this.__unloadCurrentScene();
    }
    /**
     * Reload the last scene that was loaded
     * @returns a promise that resolves to true if the scene was loaded successfully
     */
    async reload() {
        if (this.__lastSwitchScene) {
            const scene = this.__lastSwitchScene;
            this.__lastSwitchScene = undefined;
            return this.switchScene(scene);
        }
        return false;
    }
    /**
     * Switch to a scene by its AssetReference.
     * If the scene is already loaded it will be unloaded and the new scene will be loaded.
     * If the scene is already loading it will wait for the scene to be loaded.
     * If the scene is already loaded and the same scene is requested again it will return the same promise that was returned the first time the scene was requested.
     * @param scene The AssetReference of the scene to switch to
     * @returns a promise that resolves to true if the scene was loaded successfully
     * @example
     * ```ts
     * const myAssetReference = new AssetReference("scene1.glb");
     * sceneSwitcher.switchScene(myAssetReference).then(res => { console.log("Scene loaded", res); });
     * ```
     */
    async switchScene(scene) {
        if (!(scene instanceof AssetReference)) {
            const type = typeof scene;
            if (type === "string") {
                return this.select(scene);
            }
            else if (type === "number") {
                return this.select(scene);
            }
            // If we're switching an Object3D
            else if (scene && scene instanceof Object3D) {
                const i = this.scenes?.indexOf(scene);
                scene = new AssetReference(scene.name, undefined, scene);
                if (i >= 0)
                    this.scenes[i] = scene; // update the asset in the list to the new asset reference
            }
            else {
                console.warn(`[SceneSwitcher] Can't switch to scene of type ${type}`);
                return false;
            }
        }
        if (scene.url === this.sourceId) {
            console.warn("[SceneSwitcher] Can't load own scene - prevent recursive loading", this.sourceId);
            return false;
        }
        // ensure that we never run the same scene switch multiple times (at the same time) for the same requested scene
        if (this.__lastSwitchScene === scene && this.__lastSwitchScenePromise) {
            return this.__lastSwitchScenePromise;
        }
        this.__lastSwitchScene = scene;
        this.__lastSwitchScenePromise = this.__internalSwitchScene(scene);
        const res = await this.__lastSwitchScenePromise;
        return res;
    }
    async __unloadCurrentScene() {
        const current = this._currentScene;
        this._currentScene = undefined;
        if (current) {
            if (debug)
                console.log("[SceneSwitcher] UNLOAD", current.url, "HasURL?: " + current.hasUrl);
            const sceneListener = this.tryGetSceneEventListener(current.asset);
            if (sceneListener?.sceneClosing) {
                const res = sceneListener.sceneClosing();
                if (res instanceof Promise)
                    await res;
            }
            // // if the current scene has a URL (so it can be reloaded)
            // // then we unload it
            if (current.hasUrl) {
                current.unload();
                if (this._currentSceneAsset)
                    destroy(this._currentSceneAsset, true, false);
            }
            else {
                GameObject.remove(this._currentSceneAsset);
            }
        }
    }
    _currentlyLoadingScene;
    /** @internal */
    async __internalSwitchScene(scene) {
        await this.__unloadCurrentScene();
        const index = this._currentIndex = this.scenes?.indexOf(scene) ?? -1;
        try {
            if (debug)
                console.debug(`${Date.now()} [SceneSwitcher] Loading scene start: ${scene.url} (index: ${index})`);
            this._currentlyLoadingScene = scene;
            this._currentLoadingProgress = new ProgressEvent("progress", { loaded: 0, total: 1 });
            const loadStartEvt = new CustomEvent("loadscene-start", { detail: { scene: scene, switcher: this, index: index } });
            this.dispatchEvent(loadStartEvt);
            this.sceneLoadingStart?.invoke(loadStartEvt.detail);
            await this.onStartLoading();
            // start loading and wait for the scene to be loaded
            await scene.loadAssetAsync((_, prog) => {
                if (debug) {
                    const t01 = prog.loaded / prog.total;
                    const progressBarString = "[" + "=".repeat(Math.floor(t01 * 20)) + "-".repeat(20 - Math.floor(t01 * 20)) + "]";
                    console.debug(`${Date.now()} [SceneSwitcher] Loading scene progress: ${(t01 * 100).toFixed(1)} % ${progressBarString}`, scene.url);
                }
                this._currentLoadingProgress = prog;
                this.dispatchEvent(prog);
                this.sceneLoadingProgress?.invoke(prog);
            }).catch(console.error);
            await this.onEndLoading();
            const finishedEvt = new CustomEvent("loadscene-finished", { detail: { scene: scene, switcher: this, index: index } });
            this.dispatchEvent(finishedEvt);
            this._currentLoadingProgress = undefined;
            this._currentlyLoadingScene = undefined;
            if (finishedEvt.defaultPrevented) {
                if (debug)
                    console.warn("[SceneSwitcher] Adding loaded scene prevented:", scene, finishedEvt);
                return false;
            }
            if (!scene.asset) {
                if (debug)
                    console.warn("[SceneSwitcher] Failed loading scene:", scene);
                return false;
            }
            if (this._currentIndex === index) {
                if (debug)
                    console.log("[SceneSwitcher] ADD", scene.url);
                this._currentScene = scene;
                // Experimental: replace the whole content of the scene
                if (experimental_clearSceneOnLoad) {
                    const camera = this.context.mainCameraComponent?.gameObject || this.context.mainCamera;
                    camera?.removeFromParent();
                    const self = this.gameObject.removeFromParent();
                    destroy(this.context.scene, true, true);
                    this.context.scene = new Scene();
                    this.context.scene.add(self);
                    if (camera) {
                        this.context.scene.add(camera);
                    }
                }
                // @TODO: if multiple scene switcher or scenes use this asset already it will be moved
                if (!scene.asset.parent) {
                    this._currentSceneAsset = scene.asset;
                    GameObject.add(scene.asset, this.gameObject);
                }
                else {
                    this._currentSceneAsset = instantiate(scene.asset, { parent: this.gameObject });
                }
                if (this.useSceneLighting)
                    this.context.sceneLighting.enable(scene);
                // Set the background texture from the loaded scene
                if (this.useSceneBackground) {
                    const skybox = this.context.lightmaps.tryGetSkybox(scene.url);
                    if (skybox) {
                        skybox.mapping = EquirectangularReflectionMapping;
                        this.context.scene.background = skybox;
                    }
                    else if (debug) {
                        console.warn("[SceneSwitcher] Can't find skybox for scene " + scene.url);
                    }
                }
                if (this.useHistory && index >= 0) {
                    // take the index as the query parameter value
                    let queryParameterValue = index.toString();
                    // unless the user defines that he wants to use the scene name
                    if (this.useSceneName) {
                        if (scene instanceof Object3D)
                            queryParameterValue = scene.name;
                        else if (scene.url)
                            queryParameterValue = sceneUriToName(scene.url);
                    }
                    // save the loaded scene as an url parameter
                    if (this.queryParameterName?.length)
                        setParamWithoutReload(this.queryParameterName, queryParameterValue, this.useHistory);
                    // or set the history state without updating the url parameter
                    else {
                        const lastState = history.state;
                        const stateName = this.guid + "::" + index;
                        if (lastState !== stateName)
                            history.pushState(stateName, "unused", location.href);
                    }
                }
                // Call SceneListener opened callback (if a SceneListener is in the scene)
                const sceneListener = this.tryGetSceneEventListener(scene.asset);
                if (sceneListener?.sceneOpened) {
                    const res = sceneListener.sceneOpened(this);
                    if (res instanceof Promise)
                        await res;
                }
                if (debug)
                    console.debug(`${Date.now()} [SceneSwitcher] Loading scene finished: ${scene.url} (index: ${index})`);
                const openedEvt = new CustomEvent("scene-opened", { detail: { scene: scene, switcher: this, index: index } });
                this.dispatchEvent(openedEvt);
                this.sceneLoaded?.invoke(this);
                return true;
            }
        }
        catch (err) {
            console.error(err);
        }
        return false;
    }
    preload(index) {
        if (index >= 0 && index < this.scenes.length) {
            const scene = this.scenes[index];
            if (scene instanceof AssetReference)
                return scene.preload();
        }
        return couldNotLoadScenePromise;
    }
    tryLoadFromQueryParam() {
        if (!this.queryParameterName?.length)
            return couldNotLoadScenePromise;
        // try restore the scene from the url
        const value = getParam(this.queryParameterName);
        if (typeof value === "boolean")
            return couldNotLoadScenePromise;
        return this.trySelectSceneFromValue(value);
    }
    /** try to select a scene from a string or index */
    trySelectSceneFromValue(value) {
        if (typeof value === "string") {
            const index = parseInt(value);
            if (index >= 0 && index < this.scenes.length) {
                return this.select(index);
                ;
            }
            else {
                // Try to find a scene with a matching name
                // we don't care about casing. e.g. Scene1 and scene1 should both match
                const lowerCaseValue = value.toLowerCase();
                for (let i = 0; i < this.scenes.length; i++) {
                    const scene = this.scenes[i];
                    if (!scene)
                        continue;
                    const name = scene instanceof Object3D ? scene.name : sceneUriToName(scene.url);
                    if (name.toLowerCase().includes(lowerCaseValue)) {
                        return this.select(i);
                    }
                }
            }
        }
        else if (typeof value === "number") {
            if (value >= 0 && value < this.scenes.length) {
                return this.select(value);
                ;
            }
        }
        if (isLocalNetwork())
            console.warn("[SceneSwitcher] Can not find scene: \"" + value + "\"", this);
        return couldNotLoadScenePromise;
    }
    _lastLoadingScene = undefined;
    _loadingScenePromise = undefined;
    _isCurrentlyLoading = false;
    _currentLoadingProgress = undefined;
    async onStartLoading() {
        this._isCurrentlyLoading = true;
        if (this.loadingScene) {
            // save the last loading scene reference so that it can be changed at runtime
            // since we cache the loading promise here
            if (this._lastLoadingScene !== this.loadingScene) {
                this._loadingScenePromise = undefined;
            }
            this._lastLoadingScene = this.loadingScene;
            if (!this._loadingScenePromise) {
                this._loadingScenePromise = this.loadingScene?.loadAssetAsync().then(res => res != null);
            }
            await this._loadingScenePromise;
            if (this._isCurrentlyLoading && this.loadingScene?.asset) {
                if (debug)
                    console.log("Add loading scene", this.loadingScene.url, this.loadingScene.asset);
                const loadingScene = this.loadingScene.asset;
                GameObject.add(loadingScene, this.gameObject);
                const sceneListener = this.tryGetSceneEventListener(loadingScene);
                if (sceneListener?.sceneOpened) {
                    const res = sceneListener.sceneOpened(this);
                    if (res instanceof Promise)
                        await res;
                }
            }
        }
        // Invoke the event on a loading listener on the same object as the scene switcher
        if (this._isCurrentlyLoading) {
            const listener = this.tryGetSceneEventListener(this.gameObject);
            if (listener) {
                if (listener.sceneOpened) {
                    const res = listener.sceneOpened(this);
                    if (res instanceof Promise)
                        await res;
                }
            }
        }
    }
    async onEndLoading() {
        this._isCurrentlyLoading = false;
        if (this.loadingScene?.asset) {
            if (debug)
                console.log("Remove loading scene", this.loadingScene.url);
            const obj = this.loadingScene.asset;
            // try to find an ISceneEventListener component
            const sceneListener = this.tryGetSceneEventListener(obj);
            if (typeof sceneListener?.sceneClosing === "function") {
                const res = sceneListener.sceneClosing();
                if (res instanceof Promise)
                    await res;
            }
            GameObject.remove(obj);
        }
        // Invoke the event on a loading listener on the same object as the scene switcher
        if (!this._isCurrentlyLoading) {
            const listener = this.tryGetSceneEventListener(this.gameObject);
            if (listener) {
                if (listener.sceneClosing) {
                    const res = listener.sceneClosing();
                    if (res instanceof Promise)
                        await res;
                }
            }
        }
    }
    tryGetSceneEventListener(obj, level = 0) {
        if (!obj) {
            return null;
        }
        const sceneListener = GameObject.foreachComponent(obj, c => {
            const i = c;
            if (i.sceneClosing || i.sceneOpened)
                return i;
            else
                return undefined;
        });
        // if we didnt find any component with the listener on the root object
        // we also check the first level of its children because a scene might be a group
        if (level === 0 && !sceneListener && obj.children.length) {
            for (const ch of obj.children) {
                const res = this.tryGetSceneEventListener(ch, level + 1);
                if (res)
                    return res;
            }
        }
        if (!sceneListener)
            return null;
        return sceneListener;
    }
}
__decorate([
    serializable()
], SceneSwitcher.prototype, "autoLoadFirstScene", void 0);
__decorate([
    serializable(AssetReference)
], SceneSwitcher.prototype, "scenes", void 0);
__decorate([
    serializable(AssetReference)
], SceneSwitcher.prototype, "loadingScene", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "queryParameterName", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "useSceneName", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "clamp", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "useHistory", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "useKeyboard", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "useSwipe", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "useSceneLighting", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "useSceneBackground", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "preloadNext", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "preloadPrevious", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "preloadConcurrent", void 0);
__decorate([
    serializable()
], SceneSwitcher.prototype, "createMenuButtons", void 0);
__decorate([
    serializable(EventList)
], SceneSwitcher.prototype, "sceneLoadingStart", void 0);
__decorate([
    serializable(EventList)
], SceneSwitcher.prototype, "sceneLoadingProgress", void 0);
__decorate([
    serializable(EventList)
], SceneSwitcher.prototype, "sceneLoaded", void 0);
function sceneUriToName(uri) {
    const name = uri.split("/").pop();
    const value = name?.split(".").shift();
    if (value?.length)
        return value;
    return uri;
}
/**
 * PreLoadScheduler is responsible for managing preloading of scenes.
 * It handles scheduling and limiting concurrent downloads of scenes based on specified parameters.
 */
class PreLoadScheduler {
    /** Maximum number of scenes to preload ahead of the current scene */
    maxLoadAhead;
    /** Maximum number of scenes to preload behind the current scene */
    maxLoadBehind;
    /** Maximum number of scenes that can be preloaded concurrently */
    maxConcurrent;
    _isRunning = false;
    _switcher;
    _loadTasks = [];
    _maxConcurrentLoads = 1;
    /**
     * Creates a new PreLoadScheduler instance
     * @param rooms The SceneSwitcher that this scheduler belongs to
     * @param ahead Number of scenes to preload ahead of current scene
     * @param behind Number of scenes to preload behind current scene
     * @param maxConcurrent Maximum number of concurrent preloads allowed
     */
    constructor(rooms, ahead = 1, behind = 1, maxConcurrent = 2) {
        this._switcher = rooms;
        this.maxLoadAhead = ahead;
        this.maxLoadBehind = behind;
        this.maxConcurrent = maxConcurrent;
    }
    /**
     * Starts the preloading process after a specified delay
     * @param delay Time in milliseconds to wait before starting preload
     */
    begin(delay) {
        if (this._isRunning)
            return;
        if (debug)
            console.log("[SceneSwitcher] Preload scheduled", { delay });
        this._isRunning = true;
        let lastRoom = -10;
        let searchDistance;
        let searchCall;
        const array = this._switcher.scenes;
        const startTime = Date.now() + delay;
        const interval = setInterval(() => {
            if (this.allLoaded()) {
                if (debug)
                    console.log("[SceneSwitcher] All scenes (pre-)loaded");
                this.stop();
            }
            if (!this._isRunning) {
                clearInterval(interval);
                return;
            }
            if (Date.now() < startTime)
                return;
            if (this.canLoadNewScene() === false)
                return;
            if (lastRoom === -10 || lastRoom !== this._switcher.currentIndex) {
                lastRoom = this._switcher.currentIndex;
                searchCall = 0;
                searchDistance = 0;
            }
            const searchForward = searchCall % 2 === 0;
            if (searchForward)
                searchDistance += 1;
            searchCall += 1;
            const maxSearchDistance = searchForward ? this.maxLoadAhead : this.maxLoadBehind;
            if (searchDistance > maxSearchDistance)
                return;
            const roomIndex = searchForward ? lastRoom + searchDistance : lastRoom - searchDistance;
            if (roomIndex < 0)
                return;
            // if (roomIndex < 0) roomIndex = array.length + roomIndex;
            if (roomIndex < 0 || roomIndex >= array.length)
                return;
            if (!this._loadTasks.some(t => t.index === roomIndex)) {
                const scene = array[roomIndex];
                if (debug)
                    console.log("[SceneSwitcher] Schedule preload scene", { roomIndex, searchForward, lastRoom, currentIndex: this._switcher.currentIndex, tasks: this._loadTasks.length }, scene?.url);
                new LoadTask(roomIndex, scene, this._loadTasks);
            }
        }, 200);
    }
    /**
     * Stops the preloading process
     */
    stop() {
        this._isRunning = false;
    }
    /**
     * Checks if a new scene can be loaded based on current load limits
     * @returns True if a new scene can be loaded, false otherwise
     */
    canLoadNewScene() {
        return this._loadTasks.length < this._maxConcurrentLoads;
    }
    /**
     * Checks if all scenes in the SceneSwitcher have been loaded
     * @returns True if all scenes are loaded, false otherwise
     */
    allLoaded() {
        if (this._switcher.scenes) {
            for (const scene of this._switcher.scenes) {
                if (!scene?.isLoaded)
                    continue;
                if (scene.isLoaded() === false)
                    return false;
            }
        }
        return true;
    }
}
/**
 * Represents a single preloading task for a scene
 */
class LoadTask {
    /** The index of the scene in the scenes array */
    index;
    /** The AssetReference to be loaded */
    asset;
    /** The collection of active load tasks this task belongs to */
    tasks;
    /**
     * Creates a new LoadTask and begins loading immediately
     * @param index The index of the scene in the scenes array
     * @param asset The AssetReference to preload
     * @param tasks The collection of active load tasks
     */
    constructor(index, asset, tasks) {
        this.index = index;
        this.asset = asset;
        this.tasks = tasks;
        tasks.push(this);
        this.awaitLoading();
    }
    /**
     * Asynchronously loads the asset and removes this task from the active tasks list when complete
     */
    async awaitLoading() {
        if (this.asset && !this.asset.isLoaded()) {
            if (debug)
                console.log("[SceneSwitcher] Preload start: " + this.asset.url, this.index);
            await this.asset.preload();
            if (debug)
                console.log("[SceneSwitcher] Preload finished: " + this.asset.url, this.index);
        }
        const i = this.tasks.indexOf(this);
        if (i >= 0)
            this.tasks.splice(i, 1);
    }
}
//# sourceMappingURL=SceneSwitcher.js.map