import { Mesh, Object3D, TextureLoader, Vector4 } from "three";
import ThreeMeshUI from "three-mesh-ui";
import { addNewComponent } from "../../engine_components.js";
import { hasProLicense } from "../../engine_license.js";
import { OneEuroFilterXYZ } from "../../engine_math.js";
import { lookAtObject } from "../../engine_three_utils.js";
import { TypeStore } from "../../engine_typestore.js";
import { DeviceUtilities, getParam } from "../../engine_utils.js";
import { getIconTexture, isIconElement } from "../icons.js";
const debug = getParam("debugspatialmenu");
export class NeedleSpatialMenu {
    _context;
    needleMenu;
    htmlButtonsMap = new Map();
    enabled = true;
    constructor(context, menu) {
        this._context = context;
        this._context.pre_render_callbacks.push(this.preRender);
        this.needleMenu = menu;
        const optionsContainer = this.needleMenu.shadowRoot?.querySelector(".options");
        if (!optionsContainer) {
            console.error("Could not find options container in needle menu");
        }
        else {
            const watcher = new MutationObserver((mutations) => {
                if (!this.enabled)
                    return;
                if (this._context.isInXR == false && !debug)
                    return;
                for (const mutation of mutations) {
                    if (mutation.type === "childList") {
                        mutation.addedNodes.forEach((node) => {
                            this.createButtonFromHTMLNode(node);
                        });
                        mutation.removedNodes.forEach((node) => {
                            const button = node;
                            const spatialButton = this.htmlButtonsMap.get(button);
                            if (spatialButton) {
                                this.htmlButtonsMap.delete(button);
                                spatialButton.remove();
                                ThreeMeshUI.update();
                            }
                        });
                    }
                }
            });
            watcher.observe(optionsContainer, { childList: true });
        }
    }
    setEnabled(enabled) {
        this.enabled = enabled;
        if (!enabled)
            this.menu?.removeFromParent();
    }
    userRequestedMenu = false;
    /** Bring up the spatial menu. This is typically invoked from a button click.
     * The menu will show at a lower height to be easily accessible.
     * @returns true if the menu was shown, false if it can't be shown because the menu has been disabled.
     */
    setDisplay(display) {
        if (!this.enabled)
            return false;
        this.userRequestedMenu = display;
        return true;
    }
    onDestroy() {
        const index = this._context.pre_render_callbacks.indexOf(this.preRender);
        if (index > -1) {
            this._context.pre_render_callbacks.splice(index, 1);
        }
    }
    uiisDirty = false;
    markDirty() {
        this.uiisDirty = true;
    }
    _showNeedleLogo;
    showNeedleLogo(show) {
        this._showNeedleLogo = show;
    }
    _wasInXR = false;
    preRender = () => {
        if (!this.enabled) {
            this.menu?.removeFromParent();
            return;
        }
        if (debug && DeviceUtilities.isDesktop()) {
            this.updateMenu();
        }
        const xr = this._context.xr;
        const isImmersiveXR = xr?.running && (xr?.isPassThrough || xr?.isVR);
        if (!isImmersiveXR) {
            if (this._wasInXR) {
                this._wasInXR = false;
                this.onExitXR();
            }
            return;
        }
        if (!this._wasInXR) {
            this._wasInXR = true;
            this.onEnterXR();
        }
        this.updateMenu();
    };
    onEnterXR() {
        const nodes = this.needleMenu.shadowRoot?.querySelector(".options");
        if (nodes) {
            nodes.childNodes.forEach((node) => {
                this.createButtonFromHTMLNode(node);
            });
        }
    }
    onExitXR() {
        this.menu?.removeFromParent();
    }
    createButtonFromHTMLNode(node) {
        const menu = this.getMenu();
        const existing = this.htmlButtonsMap.get(node);
        if (existing) {
            existing.add();
            return;
        }
        if (node instanceof HTMLButtonElement) {
            const spatialButton = this.createButton(menu, node);
            this.htmlButtonsMap.set(node, spatialButton);
            spatialButton.add();
        }
        else if (node instanceof HTMLSlotElement) {
            node.assignedNodes().forEach((node) => {
                this.createButtonFromHTMLNode(node);
            });
        }
    }
    _menuTarget = new Object3D();
    positionFilter = new OneEuroFilterXYZ(90, .5);
    updateMenu() {
        //performance.mark('NeedleSpatialMenu updateMenu start');
        const menu = this.getMenu();
        this.handleNeedleWatermark();
        this._context.scene.add(menu);
        const camera = this._context.mainCamera;
        const xr = this._context.xr;
        const rigScale = xr?.rigScale || 1;
        if (camera) {
            const menuTargetPosition = camera.worldPosition;
            const fwd = camera.worldForward.multiplyScalar(-1);
            const showMenuThreshold = fwd.y > .6;
            const hideMenuThreshold = fwd.y > .4;
            const newVisibleState = (menu.visible ? hideMenuThreshold : showMenuThreshold) || this.userRequestedMenu;
            const becomesVisible = !menu.visible && newVisibleState;
            menu.visible = newVisibleState || (DeviceUtilities.isDesktop() && debug);
            fwd.multiplyScalar(3 * rigScale);
            menuTargetPosition.add(fwd);
            const testBecomesVisible = false; // this._context.time.frame % 200 == 0;
            if (becomesVisible || testBecomesVisible) {
                menu.position.copy(this._menuTarget.position);
                menu.position.y += 0.25;
                this._menuTarget.position.copy(menu.position);
                this.positionFilter.reset(menu.position);
                menu.quaternion.copy(this._menuTarget.quaternion);
                this.markDirty();
            }
            const distFromForwardView = this._menuTarget.position.distanceTo(menuTargetPosition);
            if (becomesVisible || distFromForwardView > 1.5 * rigScale) {
                this.ensureRenderOnTop(this.menu);
                this._menuTarget.position.copy(menuTargetPosition);
                this._context.scene.add(this._menuTarget);
                lookAtObject(this._menuTarget, this._context.mainCamera, true, true);
                this._menuTarget.removeFromParent();
            }
            this.positionFilter.filter(this._menuTarget.position, menu.position, this._context.time.time);
            const step = 5;
            this.menu?.quaternion.slerp(this._menuTarget.quaternion, this._context.time.deltaTime * step);
            this.menu?.scale.setScalar(rigScale);
        }
        if (this.uiisDirty) {
            //performance.mark('SpatialMenu.update.uiisDirty.start');
            this.uiisDirty = false;
            ThreeMeshUI.update();
            //performance.mark('SpatialMenu.update.uiisDirty.end');
            //performance.measure('SpatialMenu.update.uiisDirty', 'SpatialMenu.update.uiisDirty.start', 'SpatialMenu.update.uiisDirty.end');
        }
        //performance.mark('NeedleSpatialMenu updateMenu end');
        //performance.measure('SpatialMenu.update', 'NeedleSpatialMenu updateMenu start', 'NeedleSpatialMenu updateMenu end');
    }
    ensureRenderOnTop(obj, level = 0) {
        if (obj instanceof Mesh) {
            obj.material.depthTest = false;
            obj.material.depthWrite = false;
        }
        obj.renderOrder = 1000 + level * 2;
        for (const child of obj.children) {
            this.ensureRenderOnTop(child, level + 1);
        }
    }
    familyName = "Needle Spatial Menu";
    menu;
    get isVisible() {
        return this.menu?.visible;
    }
    getMenu() {
        if (this.menu) {
            return this.menu;
        }
        this.ensureFont();
        this.menu = new ThreeMeshUI.Block({
            boxSizing: 'border-box',
            fontFamily: this.familyName,
            height: "auto",
            fontSize: .1,
            color: 0x000000,
            lineHeight: 1,
            backgroundColor: 0xffffff,
            backgroundOpacity: .55,
            borderRadius: 1.0,
            whiteSpace: 'pre-wrap',
            flexDirection: 'row',
            alignItems: 'center',
            padding: new Vector4(.0, .05, .0, .05),
            borderColor: 0x000000,
            borderOpacity: .05,
            borderWidth: .005
        });
        // ensure the menu has a raycaster
        const raycaster = TypeStore.get("ObjectRaycaster");
        if (raycaster)
            addNewComponent(this.menu, new raycaster());
        return this.menu;
    }
    _poweredByNeedleElement;
    handleNeedleWatermark() {
        if (!this._poweredByNeedleElement) {
            this._poweredByNeedleElement = new ThreeMeshUI.Block({
                width: "auto",
                height: "auto",
                fontSize: .05,
                whiteSpace: 'pre-wrap',
                flexDirection: 'row',
                flexWrap: 'wrap',
                justifyContent: 'center',
                margin: 0.02,
                borderRadius: .02,
                padding: .02,
                backgroundColor: 0xffffff,
                backgroundOpacity: 1,
            });
            this._poweredByNeedleElement["needle:use_eventsystem"] = true;
            const onClick = new OnClick(this._context, () => globalThis.open("https://needle.tools", "_self"));
            addNewComponent(this._poweredByNeedleElement, onClick);
            const firstLabel = new ThreeMeshUI.Text({
                textContent: "Powered by",
                width: "auto",
                height: "auto",
            });
            const secondLabel = new ThreeMeshUI.Text({
                textContent: "needle",
                width: "auto",
                height: "auto",
                fontSize: .07,
                margin: new Vector4(0, 0, 0, .02),
            });
            this._poweredByNeedleElement.add(firstLabel);
            this._poweredByNeedleElement.add(secondLabel);
            this.menu?.add(this._poweredByNeedleElement);
            this.markDirty();
            // const logoObject = needleLogoAsSVGObject();
            // logoObject.position.y = 1;
            // this._context.scene.add(logoObject);
            const textureLoader = new TextureLoader();
            textureLoader.load("https://cdn.needle.tools/static/branding/poweredbyneedle.webp", (texture) => {
                if (texture) {
                    onClick.allowModifyUI = false;
                    firstLabel.removeFromParent();
                    secondLabel.removeFromParent();
                    const aspect = texture.image.width / texture.image.height;
                    this._poweredByNeedleElement?.set({
                        backgroundImage: texture,
                        backgroundOpacity: 1,
                        width: .1 * aspect,
                        height: .1
                    });
                    this.markDirty();
                }
            });
        }
        if (this.menu) {
            const index = this.menu.children.indexOf(this._poweredByNeedleElement);
            if (!this._showNeedleLogo && hasProLicense()) {
                if (index >= 0) {
                    this._poweredByNeedleElement.removeFromParent();
                    this.markDirty();
                }
            }
            else {
                this._poweredByNeedleElement.visible = true;
                this.menu.add(this._poweredByNeedleElement);
                const newIndex = this.menu.children.indexOf(this._poweredByNeedleElement);
                if (index !== newIndex) {
                    this.markDirty();
                }
            }
        }
    }
    ensureFont() {
        let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(this.familyName);
        if (!fontFamily) {
            fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(this.familyName);
            const normal = fontFamily.addVariant("normal", "normal", "https://cdn.needle.tools/static/fonts/msdf/arial/arial-msdf.json", "https://cdn.needle.tools/static/fonts/msdf/arial/arial.png");
            /** @ts-ignore */
            normal?.addEventListener('ready', () => {
                this.markDirty();
            });
        }
    }
    createButton(menu, htmlButton) {
        const buttonParent = new ThreeMeshUI.Block({
            width: "auto",
            height: "auto",
            whiteSpace: 'pre-wrap',
            flexDirection: 'row',
            flexWrap: 'wrap',
            justifyContent: 'center',
            backgroundColor: 0xffffff,
            backgroundOpacity: 0,
            padding: 0.02,
            margin: 0.01,
            borderRadius: 0.02,
            cursor: 'pointer',
            fontSize: 0.05,
        });
        const text = new ThreeMeshUI.Text({
            textContent: "",
            width: "auto",
            justifyContent: 'center',
            alignItems: 'center',
            backgroundOpacity: 0,
            backgroundColor: 0xffffff,
            fontFamily: this.familyName,
            color: 0x000000,
            borderRadius: 0.02,
            padding: .01,
        });
        buttonParent.add(text);
        buttonParent["needle:use_eventsystem"] = true;
        const onClick = new OnClick(this._context, () => htmlButton.click());
        addNewComponent(buttonParent, onClick);
        const spatialButton = new SpatialButton(this, menu, htmlButton, buttonParent, text);
        return spatialButton;
    }
}
class SpatialButton {
    menu;
    root;
    htmlbutton;
    spatialContainer;
    spatialText;
    spatialIcon;
    constructor(menu, root, htmlbutton, buttonContainer, buttonText) {
        this.menu = menu;
        this.root = root;
        this.htmlbutton = htmlbutton;
        this.spatialContainer = buttonContainer;
        this.spatialText = buttonText;
        const styleObserver = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                if (mutation.type === "attributes") {
                    if (mutation.attributeName === "style") {
                        this.updateVisible();
                    }
                }
                else if (mutation.type === "childList") {
                    this.updateText();
                }
            }
        });
        // watch attributes and content
        styleObserver.observe(htmlbutton, { attributes: true, childList: true });
        this.updateText();
    }
    add() {
        if (this.spatialContainer.parent != this.root) {
            this.root.add(this.spatialContainer);
            this.menu.markDirty();
            this.updateVisible();
            this.updateText();
        }
    }
    remove() {
        if (this.spatialContainer.parent) {
            this.spatialContainer.removeFromParent();
            this.menu.markDirty();
        }
    }
    updateVisible() {
        const wasVisible = this.spatialContainer.visible;
        this.spatialContainer.visible = this.htmlbutton.style.display !== "none";
        if (wasVisible !== this.spatialContainer.visible) {
            this.menu.markDirty();
        }
    }
    _lastText = "";
    updateText() {
        let newText = "";
        let iconToCreate = "";
        this.htmlbutton.childNodes.forEach((child) => {
            if (child.nodeType === Node.TEXT_NODE) {
                newText += child.textContent;
            }
            else if (child instanceof HTMLElement && isIconElement(child) && child.textContent) {
                iconToCreate = child.textContent;
            }
        });
        if (this._lastText !== newText) {
            this._lastText = newText;
            this.spatialText.name = newText;
            this.spatialText.set({ textContent: newText });
            this.menu.markDirty();
        }
        if (newText.length <= 0) {
            if (this.spatialText.parent) {
                this.spatialText.removeFromParent();
                this.menu.markDirty();
            }
        }
        else {
            if (!this.spatialText.parent) {
                this.spatialContainer.add(this.spatialText);
                this.menu.markDirty();
            }
        }
        if (iconToCreate) {
            this.createIcon(iconToCreate);
        }
    }
    _lastTexture;
    async createIcon(str) {
        if (!this.spatialIcon) {
            const texture = await getIconTexture(str);
            if (texture && !this.spatialIcon) {
                const size = 0.08;
                const icon = new ThreeMeshUI.Block({
                    width: size,
                    height: size,
                    backgroundColor: 0xffffff,
                    backgroundImage: texture,
                    backgroundOpacity: 1,
                    margin: new Vector4(0, .005, 0, 0),
                });
                this.spatialIcon = icon;
                this.spatialContainer.add(icon);
                this.menu.markDirty();
            }
        }
        if (str != this._lastTexture) {
            this._lastTexture = str;
            const texture = await getIconTexture(str);
            if (texture) {
                this.spatialIcon?.set({ backgroundImage: texture });
                this.menu.markDirty();
            }
        }
        // make sure the icon is at the first index
        const index = this.spatialContainer.children.indexOf(this.spatialIcon);
        if (index > 0) {
            this.spatialContainer.children.splice(index, 1);
            this.spatialContainer.children.unshift(this.spatialIcon);
            this.menu.markDirty();
        }
    }
}
// TODO: perhaps we should have a basic IComponent implementation in the engine folder to be able to write this more easily. OR possibly reduce the IComponent interface to the minimum
class OnClick {
    isComponent = true;
    enabled = true;
    get activeAndEnabled() { return true; }
    __internalAwake() { }
    __internalEnable() { }
    __internalDisable() { }
    __internalStart() { }
    onEnable() { }
    onDisable() { }
    gameObject;
    allowModifyUI = true;
    get element() {
        return this.gameObject;
    }
    context;
    onclick;
    constructor(context, onclick) {
        this.context = context;
        this.onclick = onclick;
    }
    onPointerEnter() {
        this.context.input.setCursor("pointer");
        if (this.allowModifyUI) {
            this.element.set({ backgroundOpacity: 1 });
            ThreeMeshUI.update();
        }
    }
    onPointerExit() {
        this.context.input.unsetCursor("pointer");
        if (this.allowModifyUI) {
            this.element.set({ backgroundOpacity: 0 });
            ThreeMeshUI.update();
        }
    }
    onPointerDown(e) {
        e.use();
    }
    onPointerUp(e) {
        e.use();
    }
    onPointerClick(e) {
        e.use();
        this.onclick();
    }
}
//# sourceMappingURL=needle-menu-spatial.js.map