import { isDevEnvironment, showBalloonWarning } from "../debug/index.js";
import { PUBLIC_KEY, VERSION } from "../engine_constants.js";
import { registerLoader } from "../engine_gltf.js";
import { hasCommercialLicense } from "../engine_license.js";
import { onStart } from "../engine_lifecycle_api.js";
import { setDracoDecoderPath, setDracoDecoderType, setKtx2TranscoderPath } from "../engine_loaders.gltf.js";
import { NeedleLoader } from "../engine_loaders.js";
import { Context } from "../engine_setup.js";
import { nameToThreeTonemapping } from "../engine_tonemapping.js";
import { getParam } from "../engine_utils.js";
import { RGBAColor } from "../js-extensions/RGBAColor.js";
import { ensureFonts } from "./fonts.js";
import { arContainerClassName, AROverlayHandler } from "./needle-engine.ar-overlay.js";
import { calculateProgress01, EngineLoadingView } from "./needle-engine.loading.js";
// registering loader here too to make sure it's imported when using engine via vanilla js
registerLoader(NeedleLoader);
const debug = getParam("debugwebcomponent");
const htmlTagName = "needle-engine";
const vrContainerClassName = "vr";
const desktopContainerClassname = "desktop";
const knownClasses = [arContainerClassName, vrContainerClassName, desktopContainerClassname];
const arSessionActiveClassName = "ar-session-active";
const desktopSessionActiveClassName = "desktop-session-active";
// #region Observables
/** Keep in sync with the overloads and docs here:
 * - {@link NeedleEngineWebComponent.setAttribute}
 * - {@link NeedleEngineWebComponent.getAttribute}
 * - {@link NeedleEngineWebComponent.addEventListener}
 * - Docs in {@link [custom-elements.json](../../../custom-elements.json)}
 * - {@link NeedleEngineWebComponent.attributeChangedCallback}
 *
 * Effectively, attributes used with these methods are also observable:
 * -  {@link registerObservableAttribute} or
 * -  {@link addAttributeChangeCallback}
 * */
const observedAttributes = [
    // MainAttributes
    "src",
    "hash",
    "camera-controls",
    "dracoDecoderPath",
    "dracoDecoderType",
    "ktx2DecoderPath",
    // keep-alive: effectively observed because read when needed
    // private
    "public-key",
    "version",
    // RenderingAttributes
    "tone-mapping",
    "tone-mapping-exposure",
    "background-blurriness",
    "background-color",
    "environment-intensity",
    // background-image: registered in Skybox.ts
    // environment-image: registered in Skybox.ts
    // scene: registered in SceneSwitcher.ts
    // clickthrough: registered in Clickthrough.ts
    "focus-rect",
    // Events
    "loadstart",
    "progress",
    "loadfinished",
];
// https://developers.google.com/web/fundamentals/web-components/customelements
/**
 * The `<needle-engine>` web component. See {@link NeedleEngineAttributes} attributes for supported attributes
 * The web component creates and manages a Needle Engine context, which is responsible for rendering a 3D scene using threejs.
 * The context is created when the `src` attribute is set, and disposed when the element is removed from the DOM. You can prevent cleanup by setting the `keep-alive` attribute to `true`.
 * The context is accessible from the `<needle-engine>` element: `document.querySelector("needle-engine").context`.
 * See {@link https://engine.needle.tools/docs/reference/needle-engine-attributes}
 *
 * @example Basic usage
 * ```html
 * <needle-engine src="https://example.com/scene.glb"></needle-engine>
 * ```
 *
 * @example With camera controls disabled
 * ```html
 * <needle-engine src="https://example.com/scene.glb" camera-controls="false"></needle-engine>
 * ```
 *
 * @see {@link NeedleButtonElement} for adding AR/VR/Quicklook buttons via &lt;needle-button&gt;
 * @see {@link NeedleMenu} for the built-in menu configuration component
 */
export class NeedleEngineWebComponent extends HTMLElement {
    static get observedAttributes() {
        return observedAttributes;
    }
    get loadingProgress01() { return this._loadingProgress01; }
    get loadingFinished() { return this.loadingProgress01 > .999; }
    /**
     * If set to false the camera controls are disabled. Default is true.
     * @type {boolean | null}
     * @memberof NeedleEngineAttributes
     * @example
     * <needle-engine camera-controls="false"></needle-engine>
     * @example
     * <needle-engine camera-controls="true"></needle-engine>
     * @example
     * <needle-engine camera-controls></needle-engine>
     * @example
     * <needle-engine></needle-engine>
     * @returns {boolean | null} if the attribute is not set it returns null
     */
    get cameraControls() {
        const attr = this.getAttribute("camera-controls");
        if (attr == null)
            return null;
        if (attr === null || attr === "False" || attr === "false" || attr === "0" || attr === "none")
            return false;
        return true;
    }
    set cameraControls(value) {
        if (value === null)
            this.removeAttribute("camera-controls");
        else
            this.setAttribute("camera-controls", value ? "true" : "false");
    }
    /**
     * Get the current context for this web component instance. The context is created when the src attribute is set and the loading has finished.
     * The context is disposed when the needle engine is removed from the document (you can prevent this by setting the keep-alive attribute to true).
     * @returns a promise that resolves to the context when the loading has finished
     */
    getContext() {
        return new Promise((res, _rej) => {
            if (this._context && this.loadingFinished) {
                res(this._context);
            }
            else {
                const cb = () => {
                    this.removeEventListener("loadfinished", cb);
                    if (this._context && this.loadingFinished) {
                        res(this._context);
                    }
                };
                this.addEventListener("loadfinished", cb);
            }
        });
    }
    /**
     * Get the context that is created when the src attribute is set and the loading has finished.
     */
    get context() { return this._context; }
    _context;
    _overlay_ar;
    _loadingProgress01 = 0;
    _loadingView;
    _previousSrc = null;
    /** @private set to true after <needle-engine> did load completely at least once. Set to false when < to false when <needle-engine> is removed from the document removed from the document */
    _didFullyLoad = false;
    _didInitialize = false;
    constructor() {
        super();
        this.attachShadow({ mode: 'open', delegatesFocus: true });
        const template = document.createElement('template');
        template.innerHTML = `<style>
    @import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap');

    :host {
        position: absolute;
        display: block;
        width: max(600px, 100%);
        height: max(300px, 100%);
        touch-action: none;

        -webkit-tap-highlight-color: transparent;
    }

    @media (max-width: 600px) {
        :host {
            width: 100%;
        }
    }
    @media (max-height: 300px) {
        :host {
            height: 100%;
        }
    }

    :host > div.canvas-wrapper {
        width: 100%;
        height: 100%;
    }

    :host canvas {   
        position: absolute;
        user-select: none;
        -webkit-user-select: none;

        /** allow touch panning but no pinch zoom **/
        /** but this doesnt work yet: 
         * touch-action: pan-x, pan-y;
         **/
        
        -webkit-touch-callout: none;
        -webkit-user-drag: none;
        -webkit-user-modify: none;

        left: 0;
        top: 0;
    }
    :host .content {
        position: absolute;
        top: 0;
        width: 100%;
        height: 100%;
        visibility: visible;
        z-index: 500; /* < must be less than the webxr buttons element */
        pointer-events: none;
    }
    :host .overlay-content {
        position: absolute;
        user-select: auto;
        pointer-events: all;
    }
    :host slot[name="quit-ar"]:hover {
        cursor: pointer;
    }
    :host .quit-ar-button {
        position: absolute;
        /** top: env(titlebar-area-y); this doesnt work **/
        top: 60px; /** camera access needs a bit more space **/
        right: 20px;
        z-index: 9999;
    }
</style>
<div class="canvas-wrapper"> <!-- this wrapper is necessary for WebXR https://github.com/meta-quest/immersive-web-emulator/issues/55 -->
    <canvas></canvas>
</div>
<div class="content">
    <slot class="overlay-content" style="display: contents;"></slot>
</div>
`;
        this.shadowRoot.appendChild(template.content.cloneNode(true));
        // TODO: do we want to rename this event?
        this.addEventListener("ready", this.onReady);
        this.addEventListener("error", this.onError);
    }
    ensureInitialized() {
        if (!this._didInitialize) {
            this._didInitialize = true;
            this.initializeDom();
        }
    }
    // #region Init DOM
    initializeDom() {
        ensureFonts();
        this.setAttribute("role", "application");
        this.setAttribute("aria-label", "Needle Engine 3D scene");
    }
    /**
     * @internal
     */
    connectedCallback() {
        if (debug)
            console.log("<needle-engine> connected");
        this.ensureInitialized();
        this.setPublicKey();
        this.setVersion();
        // If tabindex is not defined we set it to 0 to make it focusable and reachable via keyboard. Also keyboard events will be dispatched to the element (e.g. keydown, keyup) which is used by OrbitControls
        if (this.getAttribute("tabindex") === null || this.getAttribute("tabindex") === undefined)
            this.setAttribute("tabindex", "0");
        this._overlay_ar = new AROverlayHandler();
        this.getOrCreateContext();
        this.addEventListener("xr-session-started", this.onXRSessionStarted);
        this.onSetupDesktop();
        if (!this.getAttribute("src")) {
            const global = globalThis["needle:codegen_files"];
            if (debug)
                console.log("src is null, trying to load from globalThis[\"needle:codegen_files\"]", global);
            if (global) {
                if (debug)
                    console.log("globalThis[\"needle:codegen_files\"]", global);
                this.setAttribute("src", global);
            }
        }
        if (debug)
            console.log("src", this.getAttribute("src"));
        // we have to wait because codegen does set the src attribute when it's loaded
        // which might happen after the element is connected
        // if the `src` is then still null we want to initialize the default scene
        const loadId = this._loadId;
        setTimeout(() => {
            if (this.isConnected === false)
                return;
            if (loadId !== this._loadId)
                return;
            this.onLoad();
        }, 1);
    }
    /**
     * @internal
     */
    disconnectedCallback() {
        this.removeEventListener("xr-session-started", this.onXRSessionStarted);
        this._didFullyLoad = false;
        const keepAlive = this.getAttribute("keep-alive");
        const dispose = keepAlive == undefined || (keepAlive?.length > 0 && keepAlive !== "true" && keepAlive !== "1");
        if (debug)
            console.warn("<needle-engine> disconnected, keep-alive: \"" + keepAlive + "\"", typeof keepAlive, "Dispose=", dispose);
        if (dispose) {
            if (debug)
                console.warn("<needle-engine> dispose");
            this._context?.dispose();
            this._context = null;
            this._lastSourceFiles = null;
            this._loadId += 1;
        }
        else {
            if (debug)
                console.warn("<needle-engine> is not disposed because keep-alive is set");
        }
    }
    connectedMoveCallback() {
        // we dont want needle-engine to cleanup JUST because the element is moved in the DOM. See https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#custom_element_lifecycle_callbacks
    }
    // #region attributeChanged
    /**
     * @internal
     */
    attributeChangedCallback(name, oldValue, newValue) {
        if (debug)
            console.log("attributeChangedCallback", name, oldValue, newValue);
        switch (name) {
            case "src":
                if (debug)
                    console.warn("<needle-engine src>\nchanged from \"", oldValue, "\" to \"", newValue, "\"");
                this.onLoad();
                // this._watcher?.onSourceChanged(newValue);
                break;
            case "hash":
                if (this._context) {
                    this._context.hash = newValue;
                }
                break;
            case "loadstart":
            case "progress":
            case "loadfinished":
                if (typeof newValue === "string" && newValue.length > 0) {
                    if (debug)
                        console.log(name + " attribute changed", newValue);
                    this.registerEventFromAttribute(name, newValue);
                }
                break;
            case "dracoDecoderPath":
                if (debug)
                    console.log("dracoDecoderPath", newValue);
                setDracoDecoderPath(newValue);
                break;
            case "dracoDecoderType":
                if (newValue === "wasm" || newValue === "js") {
                    if (debug)
                        console.log("dracoDecoderType", newValue);
                    setDracoDecoderType(newValue);
                }
                else
                    console.error("Invalid dracoDecoderType", newValue, "expected js or wasm");
                break;
            case "ktx2DecoderPath":
                if (debug)
                    console.log("ktx2DecoderPath", newValue);
                setKtx2TranscoderPath(newValue);
                break;
            case "tonemapping":
            case "tone-mapping":
            case "tone-mapping-exposure":
            case "background-blurriness":
            case "background-color":
            case "environment-intensity":
                {
                    this.applyAttributes();
                    break;
                }
            case "public-key": {
                if (newValue != PUBLIC_KEY)
                    this.setPublicKey();
                break;
            }
            case "version": {
                if (newValue != VERSION)
                    this.setVersion();
                break;
            }
            case "focus-rect":
                {
                    const focus_rect = this.getAttribute("focus-rect");
                    if (focus_rect) {
                        const context = this.getOrCreateContext();
                        if (focus_rect === null) {
                            context.setCameraFocusRect(null);
                        }
                        else if (typeof focus_rect === "string" && focus_rect.length > 0) {
                            const element = document.querySelector(focus_rect);
                            if (!element)
                                console.warn(`No element found for focus-rect selector: ${focus_rect}`);
                            context.setCameraFocusRect(element instanceof HTMLElement ? element : null);
                        }
                        else if (focus_rect instanceof HTMLElement) {
                            context.setCameraFocusRect(focus_rect);
                        }
                        else {
                            console.warn("Invalid focus-rect value. Expected a CSS selector string or an HTMLElement.", focus_rect);
                        }
                    }
                }
                break;
        }
    }
    /** The tonemapping setting configured as an attribute on the <needle-engine> component */
    get toneMapping() {
        const attribute = (this.getAttribute("tonemapping") || this.getAttribute("tone-mapping"));
        return attribute;
    }
    _loadId = 0;
    _abortController = null;
    _lastSourceFiles = null;
    _createContextPromise = null;
    /**
     * Check if we have a context. If not a new one is created.
     */
    getOrCreateContext() {
        if (!this._context) {
            if (debug)
                console.warn("Create new context");
            this._context = new Context({ domElement: this });
        }
        return this._context;
    }
    async onLoad() {
        if (!this.isConnected)
            return;
        const context = this.getOrCreateContext();
        const filesToLoad = this.getSourceFiles();
        if (!this.checkIfSourceHasChanged(filesToLoad, this._lastSourceFiles)) {
            return;
        }
        // Abort previous loading (if it's still running)
        if (this._abortController) {
            if (debug)
                console.warn("Abort previous loading process");
            this._abortController.abort();
            this._abortController = null;
        }
        this._lastSourceFiles = filesToLoad;
        const loadId = ++this._loadId;
        if (filesToLoad === null || filesToLoad === undefined || filesToLoad.length <= 0) {
            if (debug)
                console.warn("Clear scene", filesToLoad);
            context.clear();
            if (loadId !== this._loadId)
                return;
        }
        const alias = this.getAttribute("alias");
        this.classList.add("loading");
        // Loading start events
        const allowOverridingDefaultLoading = hasCommercialLicense();
        // default loading can be overriden by calling preventDefault in the onload start event
        this.ensureLoadStartIsRegistered();
        let useDefaultLoading = this.dispatchEvent(new CustomEvent("loadstart", {
            detail: {
                context: context,
                alias: alias
            },
            cancelable: true
        }));
        if (allowOverridingDefaultLoading) {
            // Handle the <needle-engine hide-loading-overlay> attribute
            const hideOverlay = this.getAttribute("hide-loading-overlay");
            if (hideOverlay !== null && hideOverlay !== undefined && hideOverlay !== "0") {
                useDefaultLoading = false;
            }
        }
        // for local development we allow overriding the loading screen - but we notify the user that it won't work in a deployed environment
        if (useDefaultLoading === false && !allowOverridingDefaultLoading) {
            if (!isDevEnvironment())
                useDefaultLoading = true;
            console.warn("Needle Engine: You need a commercial license to override the default loading view. Visit https://needle.tools/pricing");
            if (isDevEnvironment())
                showBalloonWarning("You need a <a target=\"_blank\" href=\"https://needle.tools/pricing\">commercial license</a> to override the default loading view. This will not work in production.");
        }
        // create the loading view idf necessary
        if (!this._loadingView && useDefaultLoading)
            this._loadingView = new EngineLoadingView(this);
        if (useDefaultLoading) {
            // Only show the loading screen immedialty if we haven't loaded anything before
            if (this._didFullyLoad !== true)
                this._loadingView?.onLoadingBegin("begin load");
            else {
                // If we have loaded a glb previously and are loading a new glb due to e.g. src change
                // we don't want to show the loading screen immediately to avoid blinking if the glb to be loaded is tiny
                // so we wait a bit and only show the loading screen if the loading takes longer than a short moment
                setTimeout(() => {
                    // If the loading progress is already above ~ 70% we also don't need to show the loading screen anymore
                    if (this._loadingView && this._loadingProgress01 < 0.3 && this._loadId === loadId)
                        this._loadingView.onLoadingBegin("begin load");
                }, 300);
            }
        }
        if (debug)
            console.warn("--------------\nNeedle Engine: Begin loading " + loadId + "\n", filesToLoad);
        this.onBeforeBeginLoading();
        const loadedFiles = [];
        const progressEventDetail = {
            context: this._context,
            name: "",
            progress: {},
            index: 0,
            count: filesToLoad.length,
            totalProgress01: this._loadingProgress01
        };
        const progressEvent = new CustomEvent("progress", { detail: progressEventDetail });
        const displayNames = new Array();
        const controller = new AbortController();
        this._abortController = controller;
        const args = {
            files: filesToLoad,
            abortSignal: controller.signal,
            onLoadingProgress: evt => {
                if (debug)
                    console.debug("Loading progress: ", evt);
                if (controller.signal.aborted)
                    return;
                const index = evt.index;
                if (!displayNames[index] && evt.name) {
                    displayNames[index] = getDisplayName(evt.name);
                }
                evt.name = displayNames[index];
                if (useDefaultLoading)
                    this._loadingView?.onLoadingUpdate(evt);
                progressEventDetail.name = evt.name;
                progressEventDetail.progress = evt.progress;
                this._loadingProgress01 = calculateProgress01(evt);
                progressEventDetail.totalProgress01 = this._loadingProgress01;
                this.dispatchEvent(progressEvent);
            },
            onLoadingFinished: (_index, file, glTF) => {
                if (debug)
                    console.debug(`Finished loading \"${file}\" (aborted? ${controller.signal.aborted})`);
                if (controller.signal.aborted)
                    return;
                if (glTF) {
                    loadedFiles.push({
                        src: file,
                        file: glTF
                    });
                }
            }
        };
        // Experimental loading blur
        handleLoadingBlur(this);
        const currentHash = this.getAttribute("hash");
        if (currentHash !== null && currentHash !== undefined)
            context.hash = currentHash;
        context.alias = alias;
        this._createContextPromise = context.create(args);
        const success = await this._createContextPromise;
        this.applyAttributes();
        if (debug)
            console.warn("--------------\nNeedle Engine: finished loading " + loadId + "\n", filesToLoad, `Aborted? ${controller.signal.aborted}`);
        if (controller.signal.aborted) {
            console.log("Loading finished but aborted...");
            return;
        }
        if (this._loadId !== loadId) {
            console.log("Load id changed during loading process");
            return;
        }
        this._loadingProgress01 = 1;
        if (useDefaultLoading && success) {
            this._loadingView?.onLoadingUpdate(1, "creating scene");
        }
        this._didFullyLoad = true;
        this.classList.remove("loading");
        this.classList.add("loading-finished");
        this.dispatchEvent(new CustomEvent("loadfinished", {
            detail: {
                context: this._context,
                src: alias,
                loadedFiles: loadedFiles,
            }
        }));
    }
    // #region applyAttributes
    applyAttributes() {
        const context = this.getOrCreateContext();
        // set tonemapping if configured
        if (context.renderer) {
            const threeTonemapping = nameToThreeTonemapping(this.toneMapping);
            if (threeTonemapping !== undefined) {
                context.renderer.toneMapping = threeTonemapping;
            }
            const exposure = this.getAttribute("tone-mapping-exposure");
            if (exposure !== null && exposure !== undefined) {
                const value = parseFloat(exposure);
                if (!isNaN(value))
                    context.renderer.toneMappingExposure = value;
            }
        }
        const backgroundBlurriness = this.getAttribute("background-blurriness");
        if (backgroundBlurriness !== null && backgroundBlurriness !== undefined) {
            const value = parseFloat(backgroundBlurriness);
            if (!isNaN(value)) {
                context.scene.backgroundBlurriness = value;
            }
        }
        const environmentIntensity = this.getAttribute("environment-intensity");
        if (environmentIntensity != undefined) {
            const value = parseFloat(environmentIntensity);
            if (!isNaN(value))
                context.scene.environmentIntensity = value;
        }
        const backgroundColor = this.getAttribute("background-color");
        if (context.renderer) {
            if (typeof backgroundColor === "string" && backgroundColor.length > 0) {
                const rgbaColor = RGBAColor.fromColorRepresentation(backgroundColor);
                if (debug)
                    console.debug("<needle-engine> background-color changed, str:", backgroundColor, "→", rgbaColor);
                context.renderer.setClearColor(rgbaColor, rgbaColor.alpha);
                context.scene.background = null;
            }
            // HACK: if we set background-color to a color and then back to null we want the background-image attribute to re-apply
            else if (this.getAttribute("background-image")) {
                this.setAttribute("background-image", this.getAttribute("background-image"));
            }
        }
    }
    onXRSessionStarted = () => {
        const context = this.getOrCreateContext();
        const xrSessionMode = context.xrSessionMode;
        if (xrSessionMode === "immersive-ar")
            this.onEnterAR(context.xrSession);
        else if (xrSessionMode === "immersive-vr")
            this.onEnterVR(context.xrSession);
        // handle session end:
        context.xrSession?.addEventListener("end", () => {
            this.dispatchEvent(new CustomEvent("xr-session-ended", { detail: { session: context.xrSession, context: this._context, sessionMode: xrSessionMode } }));
            if (xrSessionMode === "immersive-ar")
                this.onExitAR(context.xrSession);
            else if (xrSessionMode === "immersive-vr")
                this.onExitVR(context.xrSession);
        });
    };
    /** called by the context when the first frame has been rendered */
    onReady = () => this._loadingView?.onLoadingFinished();
    onError = () => this._loadingView?.setMessage("Loading failed!");
    getSourceFiles() {
        const src = this.getAttribute("src");
        if (!src)
            return [];
        let filesToLoad;
        // When using globalThis the src is an array already
        if (Array.isArray(src)) {
            filesToLoad = src;
        }
        // When assigned from codegen the src is a stringified array
        else if (src.startsWith("[") && src.endsWith("]")) {
            filesToLoad = JSON.parse(src);
        }
        // src.toString for an array produces a comma separated list
        else if (src.includes(",")) {
            filesToLoad = src.split(",");
        }
        else
            filesToLoad = [src];
        // filter out invalid or empty strings
        for (let i = filesToLoad.length - 1; i >= 0; i--) {
            const file = filesToLoad[i];
            if (file === "null" || file === "undefined" || file?.length <= 0)
                filesToLoad.splice(i, 1);
        }
        return filesToLoad;
    }
    checkIfSourceHasChanged(current, previous) {
        if (current?.length !== previous?.length)
            return true;
        if (current == null && previous !== null)
            return true;
        if (current !== null && previous == null)
            return true;
        if (current !== null && previous !== null) {
            for (let i = 0; i < current?.length; i++) {
                if (current[i] !== previous[i])
                    return true;
            }
        }
        return false;
    }
    _previouslyRegisteredMap = new Map();
    ensureLoadStartIsRegistered() {
        const attributeValue = this.getAttribute("loadstart");
        if (attributeValue)
            this.registerEventFromAttribute("loadstart", attributeValue);
    }
    registerEventFromAttribute(eventName, code) {
        const prev = this._previouslyRegisteredMap.get(eventName);
        if (prev) {
            this._previouslyRegisteredMap.delete(eventName);
            this.removeEventListener(eventName, prev);
        }
        if (typeof code === "string" && code.length > 0) {
            try {
                // indirect eval https://esbuild.github.io/content-types/#direct-eval
                const fn = (0, eval)(code);
                // const fn = new Function(newValue);
                if (typeof fn === "function") {
                    this._previouslyRegisteredMap.set(eventName, fn);
                    // @ts-ignore // not sure how to type this properly
                    this.addEventListener(eventName, evt => fn?.call(globalThis, this.getOrCreateContext(), evt));
                }
            }
            catch (err) {
                console.error("Error registering event " + eventName + "=\"" + code + "\" failed with the following error:\n", err);
            }
        }
    }
    setPublicKey() {
        if (PUBLIC_KEY && PUBLIC_KEY.length > 0)
            this.setAttribute("public-key", PUBLIC_KEY);
    }
    setVersion() {
        if (VERSION && VERSION.length > 0) {
            this.setAttribute("version", VERSION);
        }
    }
    /**
     * @internal
     */
    getAROverlayContainer() {
        return this._overlay_ar.createOverlayContainer(this);
    }
    /**
     * @internal
     */
    getVROverlayContainer() {
        for (let i = 0; i < this.children.length; i++) {
            const ch = this.children[i];
            if (ch.classList.contains("vr"))
                return ch;
        }
        return null;
    }
    /**
     * @internal
     */
    onEnterAR(session) {
        const context = this.getOrCreateContext();
        this.onSetupAR();
        const overlayContainer = this.getAROverlayContainer();
        this._overlay_ar.onBegin(context, overlayContainer, session);
        this.dispatchEvent(new CustomEvent("enter-ar", { detail: { session: session, context: context, htmlContainer: this._overlay_ar?.ARContainer } }));
    }
    /**
     * @internal
     */
    onExitAR(session) {
        const context = this.getOrCreateContext();
        this._overlay_ar.onEnd(context);
        this.onSetupDesktop();
        this.dispatchEvent(new CustomEvent("exit-ar", { detail: { session: session, context: context, htmlContainer: this._overlay_ar?.ARContainer } }));
    }
    /**
     * @internal
     */
    onEnterVR(session) {
        const context = this.getOrCreateContext();
        this.onSetupVR();
        this.dispatchEvent(new CustomEvent("enter-vr", { detail: { session: session, context: context } }));
    }
    /**
     * @internal
     */
    onExitVR(session) {
        const context = this.getOrCreateContext();
        this.onSetupDesktop();
        this.dispatchEvent(new CustomEvent("exit-vr", { detail: { session: session, context: context } }));
    }
    onSetupAR() {
        this.classList.add(arSessionActiveClassName);
        this.classList.remove(desktopSessionActiveClassName);
        const arContainer = this.getAROverlayContainer();
        if (debug)
            console.warn("onSetupAR:", arContainer);
        if (arContainer) {
            arContainer.classList.add(arSessionActiveClassName);
            arContainer.classList.remove(desktopSessionActiveClassName);
        }
        this.foreachHtmlElement(ch => this.setupElementsForMode(ch, arContainerClassName));
    }
    onSetupVR() {
        this.classList.remove(arSessionActiveClassName);
        this.classList.remove(desktopSessionActiveClassName);
        this.foreachHtmlElement(ch => this.setupElementsForMode(ch, vrContainerClassName));
    }
    onSetupDesktop() {
        this.classList.remove(arSessionActiveClassName);
        this.classList.add(desktopSessionActiveClassName);
        const arContainer = this.getAROverlayContainer();
        if (arContainer) {
            arContainer.classList.remove(arSessionActiveClassName);
            arContainer.classList.add(desktopSessionActiveClassName);
        }
        this.foreachHtmlElement(ch => this.setupElementsForMode(ch, desktopContainerClassname));
    }
    setupElementsForMode(el, currentSessionType, _session = null) {
        if (el === this._context?.renderer?.domElement)
            return;
        if (el.id === "VRButton" || el.id === "ARButton")
            return;
        const classList = el.classList;
        if (classList.contains(currentSessionType)) {
            el.style.visibility = "visible";
            if (el.style.display === "none")
                el.style.display = "block";
        }
        else {
            // only modify style for elements that have a known class (e.g. marked as vr ar desktop)
            for (const known of knownClasses) {
                if (el.classList.contains(known)) {
                    el.style.visibility = "hidden";
                    el.style.display = "none";
                }
            }
        }
    }
    foreachHtmlElement(cb) {
        for (let i = 0; i < this.children.length; i++) {
            const ch = this.children[i];
            if (ch.style)
                cb(ch);
        }
    }
    onBeforeBeginLoading() {
        const customDracoDecoderPath = this.getAttribute("dracoDecoderPath");
        if (customDracoDecoderPath) {
            if (debug)
                console.log("using custom draco decoder path", customDracoDecoderPath);
            setDracoDecoderPath(customDracoDecoderPath);
        }
        const customDracoDecoderType = this.getAttribute("dracoDecoderType");
        if (customDracoDecoderType) {
            if (debug)
                console.log("using custom draco decoder type", customDracoDecoderType);
            setDracoDecoderType(customDracoDecoderType);
        }
        const customKtx2DecoderPath = this.getAttribute("ktx2DecoderPath");
        if (customKtx2DecoderPath) {
            if (debug)
                console.log("using custom ktx2 decoder path", customKtx2DecoderPath);
            setKtx2TranscoderPath(customKtx2DecoderPath);
        }
    }
    // The ones we're using internally:
    /*
    setAttribute(name: "tabindex", value: string): void;
    */
    setAttribute(qualifiedName, value) {
        super.setAttribute(qualifiedName, value);
    }
    // The ones we're using interally:
    /*
    getAttribute(name: "autostart"): string | null;
    getAttribute(name: "tabindex"): string | null;
    */
    getAttribute(qualifiedName) {
        return super.getAttribute(qualifiedName);
    }
    // This would be better but doesn't completely solve it
    // addEventListener(type: ({} & string), listener: any, options?: boolean | AddEventListenerOptions): void;
    // The ones we're using interally:
    /*
    addEventListener(type: "error", listener: (ev: ErrorEvent) => void, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: "wheel", listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: "keydown", listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: "drop", listener: (ev: DragEvent) => void, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: "dragover", listener: (ev: DragEvent) => void, options?: boolean | AddEventListenerOptions): void;
    */
    addEventListener(type, listener, options) {
        return super.addEventListener(type, listener, options);
    }
}
if (typeof window !== "undefined" && !window.customElements.get(htmlTagName))
    window.customElements.define(htmlTagName, NeedleEngineWebComponent);
/** Quick testing for the types as declared above */
/*
const elem = document.querySelector("needle-engine")!;
elem.setAttribute("src", "model.glb");
elem.addEventListener("loadfinished", (ev) => {
    const context = ev.detail.context;
});
elem.setAttribute("dracoDecoderType", "wasm");
elem.addEventListener("enter-ar", (ev) => {
    const session = ev.detail.session;
});

elem.addEventListener("mousedown", (ev) => {
    console.log("regular mousedown event", ev);
});

const onDragOverEvent = (e: DragEvent) => {

};

elem.addEventListener("dragover", onDragOverEvent);
*/
/* END Type testing */
// #region Utility functions
function getDisplayName(str) {
    if (str.startsWith("blob:")) {
        return "blob";
    }
    const parts = str.split("/");
    let name = parts[parts.length - 1];
    // Remove params
    const paramsIndex = name.indexOf("?");
    if (paramsIndex > 0)
        name = name.substring(0, paramsIndex);
    const equalSign = name.indexOf("=");
    if (equalSign > 0)
        name = name.substring(equalSign);
    const extension = name.split(".").pop();
    const extensions = ["glb", "gltf", "usdz", "usd", "fbx", "obj", "mtl"];
    const matchedIndex = !extension ? -1 : extensions.indexOf(extension.toLowerCase());
    if (extension && matchedIndex >= 0) {
        name = name.substring(0, name.length - extension.length - 1);
    }
    name = decodeURIComponent(name);
    if (name.length > 3) {
        let displayName = "";
        let lastCharacterWasSpace = false;
        const ignoredCharacters = ["(", ")", "[", "]", "{", "}", ":", ";", ",", ".", "!", "?"];
        for (let i = 0; i < name.length; i++) {
            let c = name[i];
            if (c === "_" || c === "-")
                c = " ";
            if (c === ' ' && displayName.length <= 0)
                continue;
            const isIgnored = ignoredCharacters.includes(c);
            if (isIgnored)
                continue;
            const isFirstCharacter = displayName.length === 0;
            if (isFirstCharacter) {
                c = c.toUpperCase();
            }
            if (lastCharacterWasSpace && c === " ") {
                continue;
            }
            if (lastCharacterWasSpace) {
                c = c.toUpperCase();
            }
            lastCharacterWasSpace = false;
            displayName += c;
            if (c === " ") {
                lastCharacterWasSpace = true;
            }
        }
        if (isDevEnvironment() && name !== displayName)
            console.debug("Generated display name: \"" + name + "\" → \"" + displayName + "\"");
        return displayName.trim();
    }
    if (isDevEnvironment())
        console.debug("Loading: use default name", name);
    return name;
}
function handleLoadingBlur(needleEngineElement) {
    onStart((ctx) => {
        const userBlurSetting = needleEngineElement.getAttribute("loading-blur");
        if (userBlurSetting !== null && userBlurSetting !== "0") {
            if (ctx.domElement === needleEngineElement) {
                const promise = ctx.lodsManager.manager?.awaitLoading({
                    frames: 5,
                    signal: AbortSignal.timeout(10_000), // Limit how long the page can be blurred
                    maxPromisesPerObject: 1,
                }).catch(_ => {
                    // Ignore errors (none are expected tho...)
                });
                let blur = "20px";
                if (userBlurSetting.endsWith("px"))
                    blur = userBlurSetting;
                const duration = 170;
                // If the scene has a transparent background we apply a blur to the canvas directly to not *also* blur images
                // But don't always use this effect because the edges don't look as good as with a backdrop filter
                if (ctx.scene.background === null) {
                    const domElement = needleEngineElement;
                    const canvas = ctx.renderer.domElement;
                    const originalFilterValue = canvas.style.filter;
                    const originalOverflowValue = canvas.style.overflow;
                    canvas.style.filter += `blur(${blur})`;
                    domElement.style.overflow = "hidden";
                    promise?.then(() => {
                        const animation = canvas.animate([{
                                filter: "blur(0px)",
                            }
                        ], { duration: duration, easing: "ease-in", });
                        animation.onfinish = () => {
                            canvas.style.filter = originalFilterValue;
                            domElement.style.overflow = originalOverflowValue;
                        };
                    });
                }
                else {
                    const blurryElement = document.createElement("div");
                    ctx.domElement.prepend(blurryElement);
                    blurryElement.style.cssText = "position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; pointer-events: none";
                    blurryElement.style.backdropFilter = `blur(${blur})`;
                    promise?.then(() => {
                        const animation = blurryElement.animate([{
                                backdropFilter: "blur(0px)",
                                opacity: 0,
                            }
                        ], { duration: duration, easing: "ease-in", });
                        animation.onfinish = () => {
                            blurryElement.remove();
                        };
                    });
                }
            }
        }
    }, { once: true });
}
//# sourceMappingURL=needle-engine.js.map