import type { Context } from "../../engine_context.js";
import { hasCommercialLicense, onLicenseCheckResultChanged } from "../../engine_license.js";
import { isLocalNetwork } from "../../engine_networking_utils.js";
import { DeviceUtilities, getParam } from "../../engine_utils.js";
import { onXRSessionStart, XRSessionEventArgs } from "../../xr/events.js";
import { ButtonsFactory } from "../buttons.js";
import { ensureFonts, iconFontUrl, loadFont } from "../fonts.js";
import { getIconElement } from "../icons.js";
import { NeedleLogoElement } from "../logo-element.js";
import { NeedleSpatialMenu } from "./needle-menu-spatial.js";

const elementName = "needle-menu";
const debug = getParam("debugmenu");
const debugNonCommercial = getParam("debugnoncommercial");

/** This is the model for the postMessage event that the needle engine will send to create menu items */
export declare type NeedleMenuPostMessageModel = {
    type: "needle:menu",
    button?: {
        label?: string,
        /** Google icon name */
        icon?: string,
        /** currently only URLs are supported */
        onclick?: string,
        target?: "_blank" | "_self" | "_parent" | "_top",
        /** Low priority is icon is on the left, high priority is icon is on the right. Default is 0 */
        priority?: number,
    }
}

/**
 * Used by the NeedleMenuElement to create a button at {@link NeedleMenuElement#appendChild}
 */
export declare type ButtonInfo = {
    /** Invoked when the button is clicked */
    onClick: (evt: Event) => void,
    /** Visible button text */
    label: string,
    /** Material icon name: https://fonts.google.com/icons */
    icon?: string,
    /**  "left" or "right" to place the icon on the left or right side of the button. Default is "left" */
    iconSide?: "left" | "right",
    /** Low priority is icon is on the left, high priority is icon is on the right. Default is undefined */
    priority?: number;
    /** Experimental. Allows to put two buttons in one row for the compact layout */
    class?: "row2";
    title?: string;
}

/**
 * The NeedleMenu is a menu that can be displayed in the needle engine webcomponent or in VR/AR sessions.
 * The menu can be used to add buttons to the needle engine that can be used to interact with the application.  
 * The menu can be positioned at the top or the bottom of the needle engine webcomponent  
 * 
 * @example Create a button using the NeedleMenu
 * ```typescript
 * onStart(ctx => {
 *   ctx.menu.appendChild({ 
 *    label: "Open Google",
 *    icon: "google",
 *    onClick: () => { window.open("https://www.google.com", "_blank") }
 *   });
 * })
 * ```
 * 
 * Buttons can be added to the menu using the {@link NeedleMenu#appendChild} method or by sending a postMessage event to the needle engine with the type "needle:menu". Use the {@link NeedleMenuPostMessageModel} model to create buttons with postMessage.
 * @example Create a button using a postmessage
 * ```javascript
 * window.postMessage({
 *    type: "needle:menu",
 *    button: {
 *      label: "Open Google",
 *      icon: "google",
 *      onclick: "https://www.google.com",
 *      target: "_blank",
 *    }
 * }, "*");
 * ```
 */
export class NeedleMenu {
    private readonly _context: Context;
    private readonly _menu: NeedleMenuElement;
    private readonly _spatialMenu: NeedleSpatialMenu;

    constructor(context: Context) {
        this._menu = NeedleMenuElement.getOrCreate(context.domElement, context);
        this._context = context;
        this._spatialMenu = new NeedleSpatialMenu(context, this._menu);
        window.addEventListener("message", this.onPostMessage);
        onXRSessionStart(this.onStartXR);
    }

    /** @ignore internal method */
    onDestroy() {
        window.removeEventListener("message", this.onPostMessage);
        this._menu.remove();
        this._spatialMenu.onDestroy();
    }

    private onPostMessage = (e: MessageEvent) => {
        // lets just allow the same origin for now
        if (e.origin !== globalThis.location.origin) return;
        if (typeof e.data === "object") {
            const data = e.data as NeedleMenuPostMessageModel;
            const type = data.type;
            if (type === "needle:menu") {
                const buttoninfo = data.button;
                if (buttoninfo) {
                    if (!buttoninfo.label) return console.error("NeedleMenu: buttoninfo.label is required");
                    if (!buttoninfo.onclick) return console.error("NeedleMenu: buttoninfo.onclick is required");
                    const button = document.createElement("button");
                    button.textContent = buttoninfo.label;
                    if (buttoninfo.icon) {
                        const icon = getIconElement(buttoninfo.icon);
                        button.prepend(icon);
                    }
                    if (buttoninfo.priority) {
                        button.setAttribute("priority", buttoninfo.priority.toString());
                    }
                    button.onclick = () => {
                        if (buttoninfo.onclick) {
                            const isLink = buttoninfo.onclick.startsWith("http") || buttoninfo.onclick.startsWith("www.");
                            const target = buttoninfo.target || "_blank";
                            if (isLink) {
                                globalThis.open(buttoninfo.onclick, target);
                            }
                            else console.error("NeedleMenu: onclick is not a valid link", buttoninfo.onclick);
                        }
                    }
                    this._menu.appendChild(button);
                }
                else if (debug) console.error("NeedleMenu: unknown postMessage event", data);
            }
            else if (debug) console.warn("NeedleMenu: unknown postMessage type", type, data);
        }
    };

    private onStartXR = (args: XRSessionEventArgs) => {
        if (args.session.isScreenBasedAR) {
            this._menu["previousParent"] = this._menu.parentNode;
            this._context.arOverlayElement.appendChild(this._menu);
            args.session.session.addEventListener("end", this.onExitXR);

            // Close the foldout if it's open on entering AR
            this._menu.closeFoldout();
        }
    }

    private onExitXR = () => {
        if (this._menu["previousParent"]) {
            this._menu["previousParent"].appendChild(this._menu);
            delete this._menu["previousParent"];
        }
    }

    /** Experimental: Change the menu position to be at the top or the bottom of the needle engine webcomponent
     * @param position "top" or "bottom"
     */
    setPosition(position: "top" | "bottom") {
        this._menu.setPosition(position);
    }

    /**
     * Call to show or hide the menu.  
     * NOTE: Hiding the menu is a PRO feature and requires a needle engine license. Hiding the menu will not work in production without a license.
     */
    setVisible(visible: boolean) {
        this._menu.setVisible(visible);
    }

    /** When set to false, the Needle Engine logo will be hidden. Hiding the logo requires a needle engine license */
    showNeedleLogo(visible: boolean) {
        this._menu.showNeedleLogo(visible);
        this._spatialMenu?.showNeedleLogo(visible);
        // setTimeout(()=>this.showNeedleLogo(!visible), 1000);
    }
    /** @returns true if the logo is visible */
    get logoIsVisible() {
        return this._menu.logoIsVisible;
    }
    /** When enabled=true the menu will be visible in VR/AR sessions */
    showSpatialMenu(enabled: boolean) {
        this._spatialMenu.setEnabled(enabled);
    }

    setSpatialMenuVisible(display: boolean) {
        this._spatialMenu.setDisplay(display);
    }

    get spatialMenuIsVisible() {
        return this._spatialMenu.isVisible;
    }

    /**
     * Call to add or remove a button to the menu to show a QR code for the current page  
     * If enabled=true then a button will be added to the menu that will show a QR code for the current page when clicked.
     */
    showQRCodeButton(enabled: boolean | "desktop-only"): HTMLButtonElement | null {
        if (enabled === "desktop-only") {
            enabled = !DeviceUtilities.isMobileDevice();
        }
        if (!enabled) {
            const button = ButtonsFactory.getOrCreate().qrButton;
            if (button) button.style.display = "none";
            return button ?? null;
        }
        else {
            const button = ButtonsFactory.getOrCreate().createQRCode();
            button.style.display = "";
            this._menu.appendChild(button);
            return button;
        }
    }

    /** Call to add or remove a button to the menu to mute or unmute the application  
     * Clicking the button will mute or unmute the application
    */
    showAudioPlaybackOption(visible: boolean): void {
        if (!visible) {
            this._muteButton?.remove();
            return;
        }
        this._muteButton = ButtonsFactory.getOrCreate().createMuteButton(this._context);
        this._muteButton.setAttribute("priority", "100");
        this._menu.appendChild(this._muteButton);
    }
    private _muteButton?: HTMLButtonElement;


    showFullscreenOption(visible: boolean): void {
        if (!visible) {
            this._fullscreenButton?.remove();
            return;
        }
        this._fullscreenButton = ButtonsFactory.getOrCreate().createFullscreenButton(this._context);
        if (this._fullscreenButton) {
            this._fullscreenButton.setAttribute("priority", "150");
            this._menu.appendChild(this._fullscreenButton);
        }
    }
    private _fullscreenButton?: HTMLButtonElement | null;



    appendChild(child: HTMLElement | ButtonInfo) {
        return this._menu.appendChild(child);
    }

}

export class NeedleMenuElement extends HTMLElement {

    static create() {
        // https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement#is
        return document.createElement(elementName, { is: elementName });
    }

    static getOrCreate(domElement: HTMLElement, context: Context) {
        let element = domElement.querySelector(elementName) as NeedleMenuElement | null;
        if (!element && domElement.shadowRoot) {
            element = domElement.shadowRoot.querySelector(elementName);
        }
        // if no needle-menu was found in the domelement then we search the document body
        if (!element) {
            element = window.document.body.querySelector(elementName) as NeedleMenuElement | null;
        }
        if (!element) {
            // OK no menu element exists yet anywhere
            element = NeedleMenuElement.create() as NeedleMenuElement;
            if (domElement.shadowRoot)
                domElement.shadowRoot.appendChild(element);
            else
                domElement.appendChild(element);
        }
        element._domElement = domElement;
        element._context = context;
        return element as NeedleMenuElement;
    }

    private _domElement: HTMLElement | null = null;
    private _context: Context | null = null;

    constructor() {
        super();

        const template = document.createElement('template');
        // TODO: make host full size again and move the buttons to a wrapper so that we can later easily open e.g. foldouts/dropdowns / use the whole canvas space
        template.innerHTML = `<style>

        #root {
            position: absolute;
            width: auto;
            max-width: 95%;
            left: 50%;
            transform: translateX(-50%);
            top: min(20px, 10vh);
            padding: 0.3rem;
            display: flex;
            visibility: visible;
            flex-direction: row-reverse; /* if we overflow this should be right aligned so the logo is always visible */
            pointer-events: all;
            z-index: 1000;
        }

        /** hide the menu if it's empty **/
        #root.has-no-options.logo-hidden {
            display: none; 
        }

        /** using a div here because then we can change the class for placement **/
        #root.bottom {
            top: auto;
            bottom: min(30px, 10vh);
        }
        #root.top {
            top: calc(.7rem + env(safe-area-inset-top));
        }
        
        .wrapper {
            position: relative;
            display: flex;
            flex-direction: row;
            justify-content: center;
            align-items: stretch;
            gap: 0px;
            padding: 0 0rem;
        }

        .wrapper > *, .options > button, .options > select, ::slotted(*) {
            position: relative;
            border: none;
            border-radius: 0;
            outline: 1px solid rgba(0,0,0,0);
            display: flex;
            justify-content: center;
            align-items: center;
            max-height: 2.3rem;
            max-width: 100%;

            /** basic font settings for all entries **/
            font-size: 1rem;
            font-family: 'Roboto Flex', sans-serif;
            font-optical-sizing: auto;
            font-weight: 500;
            font-weight: 200;
            font-variation-settings: "wdth" 100;
            color: rgb(20,20,20);
        }

        .options > select[multiple]:hover {
            max-height: 300px;
        }

        .floating-panel-style {
            background: rgba(255, 255, 255, .4);
            outline: rgb(0 0 0 / 5%) 1px solid;
            border: 1px solid rgba(255, 255, 255, .1);
            box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);
            border-radius: 1.5rem;
            /** 
             * to make nested background filter work 
             * https://stackoverflow.com/questions/60997948/backdrop-filter-not-working-for-nested-elements-in-chrome 
             **/
            &::before {
                content: '';
                position: absolute;
                width: 100%;
                height: 100%;
                top: 0;
                left: 0;
                z-index: -1;
                border-radius: 1.5rem;
                -webkit-backdrop-filter: blur(8px);
                backdrop-filter: blur(8px);
            }
        }

        a {
            color: inherit;
            text-decoration: none;
        }

        .options {
            display: flex;
            flex-direction: row;
            align-items: center;
        }

        .options > *, ::slotted(*) {
            max-height: 2.25rem;
            padding: .4rem .5rem;
        }
        
        :host .options > *, ::slotted(*) {
            background: transparent;
            border: none;
            white-space: nowrap;
            transition: all 0.1s linear .02s;
            border-radius: 1.5rem;
            user-select: none;
        }
        :host .options > *:hover, ::slotted(*:hover) {
            cursor: pointer;
            color: black;
            background: rgba(245, 245, 245, .8);
            box-shadow: inset 0 0 1rem rgba(0,0,30,.2);
            outline: rgba(0,0,0,.1) 1px solid;
        }
        :host .options > *:active, ::slotted(*:active) {
            background: rgba(255, 255, 255, .8);
            box-shadow: inset 0px 1px 1px rgba(255,255,255,.5), inset 0 0 2rem rgba(0,0,30,.2), inset 0px 2px 4px rgba(0,0,20,.5);
            transition: all 0.05s linear;
        }
        :host .options > *:focus, ::slotted(*:focus) {
            outline: rgba(255,255,255,.5) 1px solid;
        }
        :host .options > *:focus-visible, ::slotted(*:focus-visible) {
            outline: rgba(0,0,0,.5) 1px solid;
        }

        :host .options > *:disabled, ::slotted(*:disabled) {
            background: rgba(0,0,0,.05);
            color: rgba(60,60,60,.7);
            pointer-events: none;
        }

        button, ::slotted(button) {
            gap: 0.3rem;
        }

        /** XR button animation **/
        :host button.this-mode-is-requested {
            background: repeating-linear-gradient(to right, #fff 0%, #fff 40%, #aaffff 55%, #fff 80%);
            background-size: 200% auto;
            background-position: 0 100%;
            animation: AnimationName .7s ease infinite forwards;
        }
        :host button.other-mode-is-requested {
            opacity: .5;
        }
        
        @keyframes AnimationName {
            0% { background-position: 0% 0 }
            100% { background-position: -200% 0 }
        }




        .logo {
            cursor: pointer;
            padding-left: 0.6rem;
            padding-bottom: .02rem;
            margin-right: 0.5rem;
        }
        .logo-hidden {
            .logo {
                display: none;
            }
        }
        :host .has-options .logo {
            border-left: 1px solid rgba(40,40,40,.4);
            margin-left: 0.3rem;
            margin-right: 0.5rem;
        }

        .logo > span {
            white-space: nowrap;
        }



        /** COMPACT */

        /** Hide the menu button normally **/
        .compact-menu-button { display: none; }
        /** And show it when we're in compact mode **/
        .compact .compact-menu-button {
            position: relative;
            display: block;
            background: none;
            border: none;
            border-radius: 2rem;

            margin: 0;
            padding: 0 .3rem;
            padding-top: .2rem;

            z-index: 100;

            color: #000;

            &:hover {
                background: rgba(255,255,255,.2);
                cursor: pointer;
            }
            &:focus {
                outline: 1px solid rgba(255,255,255,.5);
            }
            &:focus-visible {
                outline: 1px solid rgba(0,0,0,.5);
            }
            & .expanded-click-area {
                position: absolute;
                left: 0;
                right: 0;
                top: 10%;
                bottom: 10%;
                transform: scale(1.8);
            }
        }  
        .has-no-options .compact-menu-button {
            display: none;
        }
        .open .compact-menu-button {
            background: rgba(255,255,255,.2);
        }
        .logo-visible .compact-menu-button { 
            margin-left: .2rem;
        }
        
        /** Open and hide menu **/
        .compact .foldout { 
            display: none;
        }
        .open .options, .open .foldout {
            display: flex;
            justify-content: center;
        }
        .compact .wrapper {
            padding: 0;
        }
        .compact .wrapper, .compact .options {
            height: auto;
            max-height: initial;
            flex-direction: row;
            gap: .12rem;
        }
        .compact .options { 
            flex-wrap: wrap;
            gap: .3rem;
        }
        .compact .top .options {
            height: auto;
            flex-direction: row;
        }
        .compact .bottom .wrapper {
            height: auto;
            flex-direction: column;
        }

        .compact .foldout {
            max-height: min(100ch, calc(100vh - 100px));
            overflow: auto;
            overflow-x: hidden;
            align-items: center;

            position: fixed;
            bottom: calc(100% + 5px);
            z-index: 100;
            width: auto;
            left: .2rem;
            right: .2rem;
            padding: .2rem;

        }
        .compact.logo-hidden .foldout {
            /** for when there's no logo we want to center the foldout **/
            min-width: 24ch;
            margin-left: 50%;
            transform: translateX(calc(-50% - .2rem));
        }
        
        .compact.top .foldout {
            top: calc(100% + 5px);
            bottom: auto;
        }

        ::-webkit-scrollbar {
            max-width: 7px;
            background: rgba(100,100,100,.2);
            border-radius: .2rem;
        }
        ::-webkit-scrollbar-thumb {
            background: rgba(255, 255, 255, .3);
            border-radius: .2rem;
        }
        ::-webkit-scrollbar-thumb:hover {
            background: rgb(150,150,150);
        }

        .compact .options > *, .compact .options > ::slotted(*) {
            font-size: 1.2rem;
            padding: .6rem .5rem;
            width: 100%;
        }
        .compact.has-options .logo {
            border: none;
            padding-left: 0;
            margin-left: 1rem;
            margin-bottom: .02rem;
        }
        .compact .options {
            /** e.g. if we have a very wide menu item like a select with long option names we don't want to overflow **/
            max-width: 100%;

            & > button, & > select {
                display: flex;
                flex-basis: 100%;
                min-height: 3rem;
            }
            & > button.row2 {
                //border: 1px solid red !important;
                display: flex;
                flex: 1;
                flex-basis: 30%;
            }
        }

        /** If there's really not enough space then just hide all options **/
        @media (max-width: 100px) or (max-height: 100px){
            .foldout {
                display: none !important;
            }
            .compact-menu-button {
                display: none !important;
            }
        }
        
        /* dark mode */
        /*
        @media (prefers-color-scheme: dark) {
            :host {
                background: rgba(0,0,0, .6);
            }
            :host button {
                color: rgba(200,200,200);
            }
            :host button:hover {
                background: rgba(100,100,100, .8);
            }
        }
        */

        </style>
        
        <div id="root" class="logo-hidden floating-panel-style bottom">
            <div class="wrapper">
                <div class="foldout">
                    <div class="options" part="options">
                        <slot></slot>
                    </div>
                    <div class="options" part="options">
                        <slot name="end"></slot>
                    </div>
                </div>
                <div style="user-select:none" class="logo">
                    <span class="madewith notranslate">powered by</span>
                </div>
            </div>
            <button class="compact-menu-button">
                <div class="expanded-click-area"></div>
            </button>
        </div>
        `;

        // we dont need to expose the shadow root
        const shadow = this.attachShadow({ mode: 'open' });

        // we need to add the icons to both the shadow dom as well as the HEAD to work 
        // https://github.com/google/material-design-icons/issues/1165
        ensureFonts();
        loadFont(iconFontUrl, { loadedCallback: () => { this.handleSizeChange() } });
        loadFont(iconFontUrl, { element: shadow });

        const content = template.content.cloneNode(true) as DocumentFragment;
        shadow?.appendChild(content);
        this.root = shadow.querySelector("#root") as HTMLDivElement;

        this.wrapper = this.root?.querySelector(".wrapper") as HTMLDivElement;
        this.options = this.root?.querySelector(".options") as HTMLDivElement;
        this.logoContainer = this.root?.querySelector(".logo") as HTMLDivElement;
        this.compactMenuButton = this.root?.querySelector(".compact-menu-button") as HTMLButtonElement;
        this.compactMenuButton.append(getIconElement("more_vert"));
        this.foldout = this.root?.querySelector(".foldout") as HTMLDivElement;

        this.root?.appendChild(this.wrapper);
        this.wrapper.classList.add("wrapper");

        const logo = NeedleLogoElement.create();
        logo.style.minHeight = "1rem";
        this.logoContainer.append(logo);
        this.logoContainer.addEventListener("click", () => {
            globalThis.open("https://needle.tools", "_blank");
        });

        try {
            // if the user has a license then we CAN hide the needle logo
            // calling this method immediately will cause an issue with vite bundling tho
            window.requestAnimationFrame(() => onLicenseCheckResultChanged(res => {
                if (res == true && hasCommercialLicense() && !debugNonCommercial) {
                    let visible = this._userRequestedLogoVisible;
                    if (visible === undefined) visible = false;
                    this.___onSetLogoVisible(visible);
                }
                else {
                    this.___onSetLogoVisible(true);
                }
            }));
        } catch (e) {
            console.error("[Needle Menu] License check failed.", e);
        }

        this.compactMenuButton.addEventListener("click", evt => {
            evt.preventDefault();
            this.root.classList.toggle("open");
        });


        let context = this._context;
        // we need to assign it in the timeout because the reference is set *after* the constructor did run
        setTimeout(() => context = this._context);

        // watch changes
        let changeEventCounter = 0;
        const forceVisible = (parent, visible) => {
            if (debug) console.log("Set menu visible", visible);
            if (context?.isInAR && context.arOverlayElement) {
                if (parent != context.arOverlayElement) {
                    context.arOverlayElement.appendChild(this);
                }
            }
            else if (this.parentNode != this._domElement?.shadowRoot)
                this._domElement?.shadowRoot?.appendChild(this);
            this.style.display = visible ? "flex" : "none";
            this.style.visibility = "visible";
            this.style.opacity = "1";
        }

        let isHandlingMutation = false;
        const rootObserver = new MutationObserver(mutations => {
            if (isHandlingMutation) {
                return;
            }
            try {
                isHandlingMutation = true;
                this.onChangeDetected(mutations);

                // ensure the menu is not hidden or removed
                const requiredParent = this?.parentNode;
                if (this.style.display != "flex" || this.style.visibility != "visible" || this.style.opacity != "1" || requiredParent != this._domElement?.shadowRoot) {
                    if (!hasCommercialLicense()) {
                        const change = changeEventCounter++;
                        // if a user doesn't have a local pro license *but* for development the menu is hidden then we show a warning
                        if (isLocalNetwork() && this._userRequestedMenuVisible === false) {
                            // set visible once so that the check above is not triggered again
                            if (change === 0) {
                                // if the user requested visible to false before this code is called for the first time we want to respect the choice just in this case
                                forceVisible(requiredParent, this._userRequestedMenuVisible);
                            }
                            // warn only once
                            if (change === 1) {
                                console.warn(`Needle Menu Warning: You need a PRO license to hide the Needle Engine menu → The menu will be visible in your deployed website if you don't have a PRO license. See https://needle.tools/pricing for details.`);
                            }
                        }
                        else {
                            if (change === 0) {
                                forceVisible(requiredParent, true);
                            }
                            else {
                                setTimeout(() => forceVisible(requiredParent, true), 5)
                            }
                        }
                    }
                }
            }
            finally {
                isHandlingMutation = false;
            }
        });
        rootObserver.observe(this.root, { childList: true, subtree: true, attributes: true });



        if (debug) {
            this.___insertDebugOptions();
        }
    }

    private _sizeChangeInterval;

    connectedCallback() {
        window.addEventListener("resize", this.handleSizeChange);
        this.handleMenuVisible();
        this._sizeChangeInterval = setInterval(() => this.handleSizeChange(undefined, true), 5000);
        // the dom element is set after the constructor runs
        setTimeout(() => {
            this._domElement?.addEventListener("resize", this.handleSizeChange);
            this._domElement?.addEventListener("click", this.#onClick);
        }, 1)
    }
    disconnectedCallback() {
        window.removeEventListener("resize", this.handleSizeChange);
        clearInterval(this._sizeChangeInterval);
        this._domElement?.removeEventListener("resize", this.handleSizeChange);
        this._context?.domElement.removeEventListener("click", this.#onClick);
    }

    #onClick = (e: Event) => {
        // detect a click outside the opened foldout to automatically close it
        if (!e.defaultPrevented
            && e.target == this._domElement
            && (e instanceof PointerEvent && e.button === 0)
            && this.root.classList.contains("open")) {
            // The menu is open, it's a click outside the foldout?
            const rect = this.foldout.getBoundingClientRect();
            const pointerEvent = e as PointerEvent;
            if (!(pointerEvent.clientX > rect.left
                && pointerEvent.clientX < rect.right
                && pointerEvent.clientY > rect.top
                && pointerEvent.clientY < rect.bottom)) {
                this.root.classList.toggle("open", false);
            }
        }
    }

    private _userRequestedLogoVisible?: boolean = undefined;
    showNeedleLogo(visible: boolean) {
        this._userRequestedLogoVisible = visible;
        if (!visible) {
            if (!hasCommercialLicense() || debugNonCommercial) {
                console.warn("[Needle Engine] You need a PRO license to hide the Needle Engine logo in production.");
                const localNetwork = isLocalNetwork()
                if (!localNetwork) return;
            }
        }
        this.___onSetLogoVisible(visible);
    }
    /** @returns true if the logo is visible */
    get logoIsVisible() {
        return !this.root.classList.contains("logo-hidden");
    }

    private ___onSetLogoVisible(visible: boolean) {
        this.logoContainer.style.display = "";
        this.logoContainer.style.opacity = "1";
        this.logoContainer.style.visibility = "visible";
        if (visible) {
            this.root.classList.remove("logo-hidden");
            this.root.classList.add("logo-visible");
        }
        else {
            this.root.classList.remove("logo-visible");
            this.root.classList.add("logo-hidden");
        }
    }

    setPosition(position: "top" | "bottom") {
        // ensure the position is of a known type:
        if (position !== "top" && position !== "bottom") {
            return console.error("NeedleMenu.setPosition: invalid position", position);
        }
        this.root.classList.remove("top", "bottom");
        this.root.classList.add(position);
    }

    private _userRequestedMenuVisible?: boolean = undefined;
    setVisible(visible: boolean) {
        this._userRequestedMenuVisible = visible;
        this.style.display = visible ? "flex" : "none";
    }

    /**
     * If the menu is in compact mode and the foldout is currently open (to show all menu options) then this will close the foldout
     */
    closeFoldout() {
        this.root.classList.remove("open");
    }

    // private _root: ShadowRoot | null = null;
    private readonly root: HTMLDivElement;
    /** wraps the whole content */
    private readonly wrapper: HTMLDivElement;
    /** contains the buttons and dynamic elements */
    private readonly options: HTMLDivElement;
    /** contains the needle-logo html element */
    private readonly logoContainer: HTMLDivElement;
    private readonly compactMenuButton: HTMLButtonElement;
    private readonly foldout: HTMLDivElement;

    append(...nodes: (string | Node)[]): void {
        for (const node of nodes) {
            if (typeof node === "string") {
                const element = document.createTextNode(node);
                this.options.appendChild(element);
            } else {
                this.options.appendChild(node);
            }
        }
    }
    appendChild<T extends Node>(node: T | ButtonInfo): T {

        if (!(node instanceof Node)) {
            const button = document.createElement("button");
            button.textContent = node.label;
            button.onclick = node.onClick;
            button.setAttribute("priority", node.priority?.toString() ?? "0");
            if (node.title) {
                button.title = node.title;
            }
            if (node.icon) {
                const icon = getIconElement(node.icon);
                if (node.iconSide === "right") {
                    button.appendChild(icon);
                } else {
                    button.prepend(icon);
                }
            }
            if (node.class) {
                button.classList.add(node.class);
            }
            node = button as unknown as T;
        }

        const res = this.options.appendChild(node);
        return res;
    }
    prepend(...nodes: (string | Node)[]): void {
        for (const node of nodes) {
            if (typeof node === "string") {
                const element = document.createTextNode(node);
                this.options.prepend(element);
            } else {
                this.options.prepend(node);
            }
        }
    }

    private _isHandlingChange = false;

    /** Called when any change in the web component is detected (including in children and child attributes) */
    private onChangeDetected(_mut: MutationRecord[]) {
        if (this._isHandlingChange) return;
        this._isHandlingChange = true;
        try {
            // if (debug) console.log("NeedleMenu.onChangeDetected", _mut);
            this.handleMenuVisible();
            for (const mut of _mut) {
                if (mut.target == this.options) {
                    this.onOptionsChildrenChanged(mut);
                }
            }
        }
        finally {
            this._isHandlingChange = false;
        }
    }

    private onOptionsChildrenChanged(_mut: MutationRecord) {
        this.root.classList.toggle("has-options", this.hasAnyVisibleOptions);
        this.root.classList.toggle("has-no-options", !this.hasAnyVisibleOptions);
        this.handleSizeChange(undefined, true);

        if (_mut.type === "childList") {
            if (_mut.addedNodes.length > 0) {
                const children = Array.from(this.options.children);
                children.sort((a, b) => {
                    const p1 = parseInt(a.getAttribute("priority") || "0");
                    const p2 = parseInt(b.getAttribute("priority") || "0");
                    return p1 - p2;
                });
                let sortingChanged = false;
                for (let i = 0; i < children.length; i++) {
                    const existing = this.options.children[i];
                    const child = children[i];
                    if (existing !== child) {
                        sortingChanged = true;
                        break;
                    }
                }
                if (sortingChanged) {
                    for (const child of children) {
                        this.options.appendChild(child);
                    }
                }
            }
        }
    }

    private _didSort: Map<HTMLElement, number> = new Map();


    /** checks if the menu has any content and should be rendered at all
     * if we dont have any content and logo then we hide the menu
     */
    private handleMenuVisible() {
        if (debug) console.log("Update VisibleState: Any Content?", this.hasAnyContent);
        if (this.hasAnyContent) {
            this.root.style.display = "";
        } else {
            this.root.style.display = "none";
        }
        this.root.classList.toggle("has-options", this.hasAnyVisibleOptions);
        this.root.classList.toggle("has-no-options", !this.hasAnyVisibleOptions);
    }

    /** @returns true if we have any content OR a logo */
    get hasAnyContent() {
        // is the logo visible?
        if (this.logoContainer.style.display != "none") return true;
        if (this.hasAnyVisibleOptions) return true;
        return false;
    }
    get hasAnyVisibleOptions() {
        // do we have any visible buttons?
        for (let i = 0; i < this.options.children.length; i++) {
            const child = this.options.children[i] as HTMLElement
            // is slot?
            if (child.tagName === "SLOT") {
                const slotElement = child as HTMLSlotElement;
                const nodes = slotElement.assignedNodes();
                for (const node of nodes) {
                    if (node instanceof HTMLElement) {
                        if (node.style.display != "none") return true;
                    }
                }
            }
            else if (child.style.display != "none") return true;
        }
        return false;
    }


    private _lastAvailableWidthChange = 0;
    private _timeoutHandle: number = 0;

    private handleSizeChange = (_evt?: Event, forceOrEvent?: boolean) => {
        if (!this._domElement) return;

        const width = this._domElement.clientWidth;
        if (width < 100) {
            clearTimeout(this._timeoutHandle!);
            this.root.classList.add("compact");
            this.foldout.classList.add("floating-panel-style");
            return;
        }

        const padding = 20 * 2;
        const availableWidth = width - padding;

        // if the available width has not changed significantly then we can skip the rest
        if (!forceOrEvent && Math.abs(availableWidth - this._lastAvailableWidthChange) < 1) return;
        this._lastAvailableWidthChange = availableWidth;

        clearTimeout(this._timeoutHandle!);

        this._timeoutHandle = setTimeout(() => {
            const spaceLeft = getSpaceLeft();
            if (spaceLeft < 0) {
                this.root.classList.add("compact")
                this.foldout.classList.add("floating-panel-style");
            }
            else if (spaceLeft > 0) {
                this.root.classList.remove("compact")
                this.foldout.classList.remove("floating-panel-style");
                if (getSpaceLeft() < 0) {
                    // ensure we still have enough space left
                    this.root.classList.add("compact")
                    this.foldout.classList.add("floating-panel-style");
                }
            }
        }, 5) as unknown as number;

        const getCurrentWidth = () => {
            return this.options.clientWidth + this.logoContainer.clientWidth;
        }
        const getSpaceLeft = () => {
            return availableWidth - getCurrentWidth();
        }
    }



    private ___insertDebugOptions() {
        window.addEventListener("keydown", (e) => {
            if (e.key === "p") {
                this.setPosition(this.root.classList.contains("top") ? "bottom" : "top");
            }
        });
        const removeOptionsButton = document.createElement("button");
        removeOptionsButton.textContent = "Hide Buttons";
        removeOptionsButton.onclick = () => {
            const optionsChildren = new Array(this.options.children.length);
            for (let i = 0; i < this.options.children.length; i++) {
                optionsChildren[i] = this.options.children[i];
            }
            for (const child of optionsChildren) {
                this.options.removeChild(child);
            }
            setTimeout(() => {
                for (const child of optionsChildren) {
                    this.options.appendChild(child);
                }

            }, 1000)
        };
        this.appendChild(removeOptionsButton);
        const anotherButton = document.createElement("button");
        anotherButton.textContent = "Toggle Logo";
        anotherButton.addEventListener("click", () => {
            this.logoContainer.style.display = this.logoContainer.style.display === "none" ? "" : "none";
        });
        this.appendChild(anotherButton);
    }
}


if (!customElements.get(elementName))
    customElements.define(elementName, NeedleMenuElement);