import { isDevEnvironment, showBalloonMessage } from "../../engine/debug/index.js";
import { InputEvents, PointerType } from "../../engine/engine_input.js";
import { onInitialized } from "../../engine/engine_lifecycle_api.js";
import { Mathf } from "../../engine/engine_math.js";
import { RaycastOptions } from "../../engine/engine_physics.js";
import { Context } from "../../engine/engine_setup.js";
import { getParam } from "../../engine/engine_utils.js";
import { Behaviour, GameObject } from "../Component.js";
import { hasPointerEventComponent, PointerEventData } from "./PointerEvents.js";
import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
import { UIRaycastUtils } from "./RaycastUtils.js";
import { $shadowDomOwner } from "./Symbols.js";
import { isUIObject } from "./Utils.js";
const debug = getParam("debugeventsystem");
export var EventSystemEvents;
(function (EventSystemEvents) {
    EventSystemEvents["BeforeHandleInput"] = "BeforeHandleInput";
    EventSystemEvents["AfterHandleInput"] = "AfterHandleInput";
})(EventSystemEvents || (EventSystemEvents = {}));
onInitialized((ctx) => {
    EventSystem.createIfNoneExists(ctx);
});
/**
 * [EventSystem](https://engine.needle.tools/docs/api/EventSystem) is responsible for managing and dispatching input events to UI components within the scene.
 * @summary Manages and dispatches input events to UI components
 * @category User Interface
 * @group Components
 */
export class EventSystem extends Behaviour {
    //@ts-ignore
    static ensureUpdateMeshUI(instance, context, force = false) {
        MeshUIHelper.update(instance, context, force);
    }
    static markUIDirty(_context) {
        MeshUIHelper.markDirty();
    }
    static createIfNoneExists(context) {
        if (!context.scene.getComponent(EventSystem)) {
            context.scene.addComponent(EventSystem);
        }
    }
    static get(ctx) {
        this.createIfNoneExists(ctx);
        return ctx.scene.getComponent(EventSystem);
    }
    /** Get the currently active event system */
    static get instance() {
        return this.get(Context.Current);
    }
    raycaster = [];
    register(rc) {
        if (rc && this.raycaster && !this.raycaster.includes(rc))
            this.raycaster?.push(rc);
    }
    unregister(rc) {
        const i = this.raycaster?.indexOf(rc);
        if (i !== undefined && i !== -1) {
            this.raycaster?.splice(i, 1);
        }
    }
    get hasActiveUI() {
        return this.currentActiveMeshUIComponents.length > 0;
    }
    get isHoveringObjects() { return this.hoveredByID.size > 0; }
    awake() {
        // We only want ONE eventsystem on the root scene
        // as long as this component is not implemented in core we need to check this here
        if (this.gameObject !== this.context.scene) {
            console.debug(`[Needle Engine] EventSystem is only allowed on the scene root. Disabling EventSystem on '${this.gameObject.name}'`);
            this.enabled = false;
        }
    }
    start() {
        if (!this.context.scene.getComponent(Raycaster)) {
            this.context.scene.addComponent(ObjectRaycaster);
        }
    }
    onEnable() {
        this.context.input.addEventListener(InputEvents.PointerDown, this.onPointerEvent);
        this.context.input.addEventListener(InputEvents.PointerUp, this.onPointerEvent);
        this.context.input.addEventListener(InputEvents.PointerMove, this.onPointerEvent);
    }
    onDisable() {
        this.context.input.removeEventListener(InputEvents.PointerDown, this.onPointerEvent);
        this.context.input.removeEventListener(InputEvents.PointerUp, this.onPointerEvent);
        this.context.input.removeEventListener(InputEvents.PointerMove, this.onPointerEvent);
    }
    /**
     * all pointers that have pressed something
     *
     * key: pointerId
     * value: object that was pressed, data of the pointer event, handlers that are releavant to the event
    */
    pressedByID = new Map();
    /**
     * all hovered objects
     *
     * key: pointerId
     * value: object that is hovered, data of the pointer event
     */
    hoveredByID = new Map();
    onBeforeRender() {
        this.resetMeshUIStates();
    }
    /**
     * Handle an pointer event from the input system
     */
    onPointerEvent = (pointerEvent) => {
        if (pointerEvent === undefined)
            return;
        if (pointerEvent.propagationStopped)
            return;
        if (pointerEvent.defaultPrevented)
            return;
        if (pointerEvent.used)
            return;
        // Use the pointerID, this is the touch id or the mouse (always 0, could be index of the mouse DEVICE) or the index of the XRInputSource
        const data = new PointerEventData(this.context.input, pointerEvent);
        this._currentPointerEventName = pointerEvent.type;
        data.inputSource = this.context.input;
        data.isClick = pointerEvent.isClick;
        data.isDoubleClick = pointerEvent.isDoubleClick;
        // using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
        data.isDown = pointerEvent.type == InputEvents.PointerDown;
        data.isUp = pointerEvent.type == InputEvents.PointerUp;
        data.isPressed = this.context.input.getPointerPressed(pointerEvent.pointerId);
        // raycast
        const options = new RaycastOptions();
        if (pointerEvent.hasRay) {
            options.ray = pointerEvent.ray;
        }
        else {
            options.screenPoint = this.context.input.getPointerPositionRC(pointerEvent.pointerId);
        }
        options.allowSlowRaycastFallback = pointerEvent.isClick || pointerEvent.isDoubleClick;
        const hits = this.performRaycast(options);
        if (debug) {
            if (data.isDown)
                console.log("DOWN", { id: data.pointerId, hits: hits.length });
            else if (data.isUp)
                console.log("UP", { id: data.pointerId, hits: hits.length });
            if (data.isClick)
                console.log("CLICK", { id: data.pointerId, hits: hits.length });
        }
        if (hits) {
            for (const hit of hits) {
                hit.event = pointerEvent;
                pointerEvent.intersections.push(hit);
            }
            if (pointerEvent.origin.onPointerHits) {
                pointerEvent.origin.onPointerHits({
                    sender: this,
                    event: pointerEvent,
                    hits
                });
            }
        }
        if (debug && data.isClick) {
            showBalloonMessage("EventSystem: " + data.pointerId + " - " + this.context.time.frame + " - Up:" + data.isUp + ", Down:" + data.isDown);
        }
        const evt = {
            sender: this,
            args: data,
            hasActiveUI: this.currentActiveMeshUIComponents.length > 0,
        };
        this.dispatchEvent(new CustomEvent(EventSystemEvents.BeforeHandleInput, { detail: evt }));
        // then handle the intersections and call the callbacks on the regular objects
        this.handleIntersections(hits, data);
        this.dispatchEvent(new CustomEvent(EventSystemEvents.AfterHandleInput, { detail: evt }));
    };
    _sortedHits = [];
    /**
     * cache for objects that we want to raycast against. It's cleared before each call to performRaycast invoking raycasters
     */
    _testObjectsCache = new Map();
    /** that's the raycaster that is CURRENTLY being used for raycasting (the shouldRaycastObject method uses this) */
    _currentlyActiveRaycaster = null;
    _currentPointerEventName = null;
    /**
     * Checks if an object that we encounter has an event component and if it does, we add it to our objects cache
     * If it doesnt we tell our raycasting system to ignore it and continue in the child hierarchy
     * We do this to avoid raycasts against objects that are not going to be used by the event system
     * Because there's no component callback to be invoked anyways.
     * This is especially important to avoid expensive raycasts against SkinnedMeshes
     *
     * Further optimizations would be to check what type of event we're dealing with
     * For example if an event component has only an onPointerClick method we don't need to raycast during movement events
     * */
    shouldRaycastObject = (obj) => {
        // TODO: this implementation below should be removed and we should regularly raycast objects in the scene unless marked as "do not raycast"
        // with the introduction of the mesh-bvh based raycasting the performance impact should be greatly reduced. But this needs further testing 
        const raycasterOnObject = obj && "getComponent" in obj ? obj.getComponent(Raycaster) : null;
        if (raycasterOnObject && raycasterOnObject != this._currentlyActiveRaycaster) {
            return false;
        }
        // if (this._currentPointerEventName == "pointermove") {
        //     console.log(this.context.time.frame, obj.name, obj.type, obj.guid)
        // }
        // check if this object is actually a UI shadow hierarchy object
        let uiOwner = null;
        const isUI = isUIObject(obj);
        // if yes we want to grab the actual object that is the owner of the shadow dom
        // and check that object for the event component
        if (isUI) {
            uiOwner = obj[$shadowDomOwner]?.gameObject;
        }
        // check if the object was seen previously
        if (this._testObjectsCache.has(obj) || (uiOwner && this._testObjectsCache.has(uiOwner))) {
            // if yes we check if it was previously stored as "YES WE NEED TO RAYCAST THIS"
            const prev = this._testObjectsCache.get(obj);
            if (prev === false)
                return "continue in children";
            return true;
        }
        else {
            // if the object has another raycaster component than the one that is currently raycasting, we ignore this here
            // because then this other raycaster is responsible for raycasting this object
            // const rc = GameObject.getComponent(obj, Raycaster);
            // if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return false;
            // the object was not yet seen so we test if it has an event component
            let hasEventComponent = hasPointerEventComponent(obj, this._currentPointerEventName);
            if (!hasEventComponent && uiOwner)
                hasEventComponent = hasPointerEventComponent(uiOwner, this._currentPointerEventName);
            if (hasEventComponent) {
                // it has an event component: we add it and all its children to the cache
                // we don't need to do the same for the shadow component hierarchy 
                // because the next object that will be detecting that the shadow owner was already seen
                this._testObjectsCache.set(obj, true);
                for (const ch of obj.children)
                    this.shouldRaycastObject_AddToYesCache(ch);
                return true;
            }
            this._testObjectsCache.set(obj, false);
            return "continue in children";
        }
    };
    shouldRaycastObject_AddToYesCache(obj) {
        // if the object has another raycaster component than the one that is currently raycasting, we ignore this here
        // because then this other raycaster is responsible for raycasting this object
        // const rc = GameObject.getComponent(obj, Raycaster);
        // if (rc?.activeAndEnabled && rc !== this._currentlyActiveRaycaster) return;
        this._testObjectsCache.set(obj, true);
        for (const ch of obj.children) {
            this.shouldRaycastObject_AddToYesCache(ch);
        }
    }
    /** the raycast filter is always overriden */
    performRaycast(opts) {
        if (!this.raycaster)
            return null;
        // we clear the cache of previously seen objects
        this._testObjectsCache.clear();
        this._sortedHits.length = 0;
        opts.testObject = this.shouldRaycastObject;
        for (const rc of this.raycaster) {
            if (!rc.activeAndEnabled)
                continue;
            this._currentlyActiveRaycaster = rc;
            const res = rc.performRaycast(opts);
            this._currentlyActiveRaycaster = null;
            if (res && res.length > 0) {
                // console.log(res.length, res.map(r => r.object.name));
                this._sortedHits.push(...res);
            }
        }
        this._sortedHits.sort((a, b) => {
            return a.distance - b.distance;
        });
        return this._sortedHits;
    }
    assignHitInformation(args, hit) {
        if (!hit) {
            args.intersection = undefined;
            args.point = undefined;
            args.normal = undefined;
            args.face = undefined;
            args.distance = undefined;
            args.instanceId = undefined;
        }
        else {
            args.intersection = hit;
            args.point = hit.point;
            args.normal = hit.normal;
            args.face = hit.face;
            args.distance = hit.distance;
            args.instanceId = hit.instanceId;
        }
    }
    handleIntersections(hits, args) {
        if (hits?.length) {
            hits = this.sortCandidates(hits);
            for (const hit of hits) {
                if (args.event.immediatePropagationStopped) {
                    return false;
                }
                this.assignHitInformation(args, hit);
                if (this.handleEventOnObject(hit.object, args)) {
                    return true;
                }
            }
        }
        // first invoke captured pointers
        this.assignHitInformation(args, hits?.[0]);
        this.invokePointerCapture(args);
        // pointer has not hit any object to handle
        // thus is not hovering over anything
        const hoveredData = this.hoveredByID.get(args.pointerId);
        if (hoveredData) {
            this.propagatePointerExit(hoveredData.obj, hoveredData.data, null);
        }
        this.hoveredByID.delete(args.pointerId);
        // if it was up, it means it should notify things that it down on before
        if (args.isUp) {
            this.pressedByID.get(args.pointerId)?.handlers.forEach(h => this.invokeOnPointerUp(args, h));
            this.pressedByID.delete(args.pointerId);
        }
        return false;
    }
    _sortingBuffer = [];
    _noDepthTestingResults = [];
    sortCandidates(hits) {
        // iterate over all hits and filter for nodepth objects and normal hit objects
        // the no-depth objects will be handled first starting from the closest
        // assuming the hits array is sorted by distance (closest > furthest)
        this._sortingBuffer.length = 0;
        this._noDepthTestingResults.length = 0;
        for (let i = 0; i < hits.length; i++) {
            const hit = hits[i];
            const object = hit.object;
            if (object.material) {
                if (object.material["depthTest"] === false) {
                    this._noDepthTestingResults.push(hit);
                    continue;
                }
            }
            this._sortingBuffer.push(hit);
        }
        for (const obj of this._sortingBuffer) {
            this._noDepthTestingResults.push(obj);
        }
        return this._noDepthTestingResults;
    }
    out = {};
    /**
     * Handle hit result by preparing all needed information before propagation.
     * Then calling propagate.
     */
    handleEventOnObject(object, args) {
        // ensures that invisible objects are ignored
        if (!this.testIsVisible(object)) {
            if (args.isClick && debug)
                console.log("not allowed", object);
            return false;
        }
        // Event without pointer can't be handled
        if (args.pointerId === undefined) {
            if (debug)
                console.error("Event without pointer can't be handled", args);
            return false;
        }
        // Correct the handled object to match the relevant object in shadow dom (?)
        args.object = object;
        const parent = object.parent;
        let isShadow = false;
        const clicked = args.isClick ?? false;
        let canvasGroup = null;
        // handle potential shadow dom built from three mesh ui
        if (parent && parent.isUI) {
            const pressedOrClicked = (args.isPressed || args.isClick) ?? false;
            if (parent[$shadowDomOwner]) {
                const actualGo = parent[$shadowDomOwner].gameObject;
                if (actualGo) {
                    const res = UIRaycastUtils.isInteractable(actualGo, this.out);
                    if (!res)
                        return false;
                    canvasGroup = this.out.canvasGroup ?? null;
                    const handled = this.handleMeshUIIntersection(object, pressedOrClicked);
                    if (!clicked && handled) {
                        // return true;
                    }
                    object = actualGo;
                    isShadow = true;
                }
            }
            // adding this to have a way for allowing to receive events on TMUI elements without shadow hierarchy
            // if(parent["needle:use_eventsystem"] == true){
            //     // if use_eventsystem is true, we want to handle the event
            // }
            // else if (!isShadow) {
            //     const obj = this.handleMeshUiObjectWithoutShadowDom(parent, pressedOrClicked);
            //     if (obj) return true;
            // }
        }
        if (clicked && debug)
            console.log(this.context.time.frame, object);
        // Handle OnPointerExit -> in case when we are about to hover something new
        // TODO: we need to keep track of the components that already received a PointerEnterEvent -> we can have a hierarchy where the hovered object changes but the component is on the parent and should not receive a PointerExit event because it's still hovered (just another child object)
        const hovering = this.hoveredByID.get(args.pointerId);
        const prevHovering = hovering?.obj;
        const isNewlyHovering = prevHovering !== object;
        // trigger onPointerExit
        if (isNewlyHovering && prevHovering) {
            this.propagatePointerExit(prevHovering, hovering.data, object);
        }
        // save hovered object
        const entry = this.hoveredByID.get(args.pointerId);
        if (!entry)
            this.hoveredByID.set(args.pointerId, { obj: object, data: args });
        else {
            entry.obj = object;
            entry.data = args;
        }
        // create / update pressed entry
        if (args.isDown) {
            const data = this.pressedByID.get(args.pointerId);
            if (!data)
                this.pressedByID.set(args.pointerId, { obj: object, data: args, handlers: new Set() });
            else {
                data.obj = object;
                data.data = args;
            }
        }
        if (canvasGroup === null || canvasGroup.interactable) {
            this.handleMainInteraction(object, args, prevHovering ?? null);
        }
        return true;
    }
    /**
     * Propagate up in hiearchy and call the callback for each component that is possibly a handler
     */
    propagate(object, onComponent) {
        while (true) {
            if (!object)
                break;
            GameObject.foreachComponent(object, comp => {
                // TODO: implement Stop Immediate Propagation
                onComponent(comp);
            }, false);
            // walk up
            object = object.parent;
        }
    }
    /**
     * Propagate up in hierarchy and call handlers based on the pointer event data
     */
    handleMainInteraction(object, args, prevHovering) {
        const pressedEvent = this.pressedByID.get(args.pointerId);
        const hoveredObjectChanged = prevHovering !== object;
        // TODO: should we not move this check up before we even raycast for "pointerMove" events? We dont need to do any processing if the pointer didnt move
        let isMoving = true;
        switch (args.event.pointerType) {
            case "mouse":
            case "touch":
                const posLastFrame = this.context.input.getPointerPositionLastFrame(args.pointerId);
                const posThisFrame = this.context.input.getPointerPosition(args.pointerId);
                isMoving = posLastFrame && !Mathf.approximately(posLastFrame, posThisFrame);
                break;
            case "controller":
            case "hand":
                // for hands and controller we assume they are never totally still (except for simulated environments)
                // we might want to add a threshold here (e.g. if a user holds their hand very still or controller)
                // so maybe check the angle every frame?
                break;
        }
        this.propagate(object, (behaviour) => {
            const comp = behaviour;
            if (comp.interactable === false)
                return;
            if (!comp.activeAndEnabled || !comp.enabled)
                return;
            if (comp.onPointerEnter) {
                if (hoveredObjectChanged) {
                    this.handlePointerEnter(comp, args);
                }
            }
            if (args.isDown) {
                if (comp.onPointerDown) {
                    comp.onPointerDown(args);
                    // Set the handler that we called the down event on
                    // So we can call the up event on the same handler 
                    // In a scenario where we Down on one object and Up on another
                    pressedEvent?.handlers.add(comp);
                    this.handlePointerCapture(args, comp);
                }
            }
            if (comp.onPointerMove) {
                if (isMoving)
                    comp.onPointerMove(args);
                this.handlePointerCapture(args, comp);
            }
            if (args.isUp) {
                if (comp.onPointerUp) {
                    this.invokeOnPointerUp(args, comp);
                    // We don't want to call Up twice if we Down and Up on the same object
                    // But if we Down on one and Up on another we want to call Up on the first one as well
                    // For example if the object was cloned by the Duplicatable
                    // The original component that received the down event SHOULD also receive the up event
                    pressedEvent?.handlers.delete(comp);
                }
                // handle touch onExit (touchUp) since the pointer stops existing
                // mouse onExit (mouseUp) is handled when we hover over something else / on nothing
                // Mouse 0 is always persistent
                if (comp.onPointerExit && args.event?.pointerType === PointerType.Touch) {
                    this.handlePointerExit(comp, args);
                    this.hoveredByID.delete(args.pointerId);
                }
            }
            if (args.isClick) {
                if (comp.onPointerClick) {
                    comp.onPointerClick(args);
                }
            }
        });
        // after the propagation end, call UP on any objects that were DOWNED and didn't recieve an UP while propagating
        // If user drags away from the object, then it doesn't get the UP event
        if (args.isUp) {
            pressedEvent?.handlers.forEach((handler) => {
                this.invokeOnPointerUp(args, handler);
            });
            this.pressedByID.delete(args.pointerId);
        }
    }
    /** Propagate up in hierarchy and call onPointerExit */
    propagatePointerExit(object, args, newObject) {
        this.propagate(object, (behaviour) => {
            if (!behaviour.gameObject || behaviour.destroyed)
                return;
            const inst = behaviour;
            if (inst.onPointerExit || inst.onPointerEnter) {
                // if the newly hovered object is a child of the current object, we don't want to call onPointerExit
                if (newObject && this.isChild(newObject, behaviour.gameObject)) {
                    return;
                }
                this.handlePointerExit(inst, args);
            }
        });
    }
    /** handles onPointerUp - this will also release the pointerCapture */
    invokeOnPointerUp(evt, handler) {
        handler.onPointerUp?.call(handler, evt);
        this.releasePointerCapture(evt, handler);
    }
    /** Responsible for invoking onPointerEnter (and updating onPointerExit). We invoke onPointerEnter once per active pointerId */
    handlePointerEnter(comp, args) {
        if (comp.onPointerEnter) {
            if (this.updatePointerState(comp, args.pointerId, this.pointerEnterSymbol, true)) {
                comp.onPointerEnter(args);
            }
        }
        this.updatePointerState(comp, args.pointerId, this.pointerExitSymbol, false);
    }
    /** Responsible for invoking onPointerExit (and updating onPointerEnter). We invoke onPointerExit once per active pointerId */
    handlePointerExit(comp, evt) {
        if (comp.onPointerExit) {
            if (this.updatePointerState(comp, evt.pointerId, this.pointerExitSymbol, true)) {
                comp.onPointerExit(evt);
            }
        }
        this.updatePointerState(comp, evt.pointerId, this.pointerEnterSymbol, false);
    }
    /** updates the pointer state list for a component
     * @param comp the component to update
     * @param pointerId the pointerId to update
     * @param symbol the symbol to use for the state
     * @param add if true, the pointerId is added to the state list, if false the pointerId will be removed
     */
    updatePointerState(comp, pointerId, symbol, add) {
        let state = comp[symbol];
        if (add) {
            // the pointer is already in the state list
            if (state && state.includes(pointerId))
                return false;
            state = state || [];
            state.push(pointerId);
            comp[symbol] = state;
            return true;
        }
        else {
            if (!state || !state.includes(pointerId))
                return false;
            const i = state.indexOf(pointerId);
            if (i !== -1) {
                state.splice(i, 1);
            }
            return true;
        }
    }
    /** the list of component handlers that requested pointerCapture for a specific pointerId */
    _capturedPointer = {};
    /** check if the event was marked to be captured: if yes add the current component to the captured list */
    handlePointerCapture(evt, comp) {
        if (evt.z__pointer_ctured) {
            evt.z__pointer_ctured = false;
            const id = evt.pointerId;
            // only the onPointerMove event is called with captured pointers so we don't need to add it to our list if it doesnt implement onPointerMove
            if (comp.onPointerMove) {
                const list = this._capturedPointer[id] || [];
                list.push(comp);
                this._capturedPointer[id] = list;
            }
            else {
                if (isDevEnvironment() && !comp["z__warned_no_pointermove"]) {
                    comp["z__warned_no_pointermove"] = true;
                    console.warn("PointerCapture was requested but the component doesn't implement onPointerMove. It will not receive any pointer events");
                }
            }
        }
        else if (evt.z__pointer_cture_rleased) {
            evt.z__pointer_cture_rleased = false;
            this.releasePointerCapture(evt, comp);
        }
    }
    /** removes the component from the pointer capture list */
    releasePointerCapture(evt, component) {
        const id = evt.pointerId;
        if (this._capturedPointer[id]) {
            const i = this._capturedPointer[id].indexOf(component);
            if (i !== -1) {
                this._capturedPointer[id].splice(i, 1);
                if (debug)
                    console.log("released pointer capture", id, component, this._capturedPointer);
            }
        }
    }
    /** invoke the pointerMove event on all captured handlers */
    invokePointerCapture(evt) {
        if (evt.event.type === InputEvents.PointerMove) {
            const id = evt.pointerId;
            const captured = this._capturedPointer[id];
            if (captured) {
                if (debug)
                    console.log("Captured", id, captured);
                for (let i = 0; i < captured.length; i++) {
                    const handler = captured[i];
                    // check if it was destroyed
                    const comp = handler;
                    if (comp.destroyed) {
                        captured.splice(i, 1);
                        i--;
                        continue;
                    }
                    // invoke pointer move
                    handler.onPointerMove?.call(handler, evt);
                }
            }
        }
    }
    pointerEnterSymbol = Symbol("pointerEnter");
    pointerExitSymbol = Symbol("pointerExit");
    isChild(obj, possibleChild) {
        if (!obj || !possibleChild)
            return false;
        if (obj === possibleChild)
            return true;
        if (!obj.parent)
            return false;
        return this.isChild(obj.parent, possibleChild);
    }
    handleMeshUiObjectWithoutShadowDom(obj, pressed) {
        if (!obj || !obj.isUI)
            return true;
        const hit = this.handleMeshUIIntersection(obj, pressed);
        return hit;
    }
    currentActiveMeshUIComponents = [];
    handleMeshUIIntersection(meshUiObject, pressed) {
        const res = MeshUIHelper.updateState(meshUiObject, pressed);
        if (res) {
            this.currentActiveMeshUIComponents.push(res);
        }
        return res !== null;
    }
    resetMeshUIStates() {
        if (this.context.input.getPointerPressedCount() > 0) {
            MeshUIHelper.resetLastSelected();
        }
        if (!this.currentActiveMeshUIComponents || this.currentActiveMeshUIComponents.length <= 0)
            return;
        for (let i = 0; i < this.currentActiveMeshUIComponents.length; i++) {
            const comp = this.currentActiveMeshUIComponents[i];
            MeshUIHelper.resetState(comp);
        }
        this.currentActiveMeshUIComponents.length = 0;
    }
    testIsVisible(obj) {
        if (!obj)
            return true;
        if (!GameObject.isActiveSelf(obj))
            return false;
        return this.testIsVisible(obj.parent);
    }
}
class MeshUIHelper {
    static lastSelected = null;
    static lastUpdateFrame = [];
    static needsUpdate = false;
    static markDirty() {
        this.needsUpdate = true;
    }
    static update(threeMeshUI, context, force = false) {
        if (force) {
            threeMeshUI.update();
            return;
        }
        const currentFrame = context.time.frameCount;
        for (const lu of this.lastUpdateFrame) {
            if (lu.context === context) {
                if (currentFrame === lu.frame)
                    return;
                lu.frame = currentFrame;
                let shouldUpdate = this.needsUpdate || currentFrame < 1;
                if (lu.nextUpdate <= currentFrame)
                    shouldUpdate = true;
                // if (this.needsUpdate) lu.nextUpdate = currentFrame + 3;
                if (shouldUpdate) {
                    // console.warn(currentFrame, lu.nextUpdate, this.needsUpdate)
                    if (debug)
                        console.log("Update threemeshui");
                    this.needsUpdate = false;
                    lu.nextUpdate = currentFrame + 60;
                    threeMeshUI.update();
                }
                return;
            }
        }
        this.lastUpdateFrame = [{ context, frame: currentFrame, nextUpdate: currentFrame + 60 }];
        threeMeshUI.update();
        this.needsUpdate = false;
    }
    static updateState(intersect, _selectState) {
        let foundBlock = null;
        if (intersect) {
            foundBlock = this.findBlockOrTextInParent(intersect);
            // console.log(intersect, "-- found block:", foundBlock)
            if (foundBlock && foundBlock !== this.lastSelected) {
                const interactable = foundBlock["interactable"];
                if (interactable === false)
                    return null;
                this.needsUpdate = true;
            }
        }
        return foundBlock;
    }
    static resetLastSelected() {
        const last = this.lastSelected;
        if (!last)
            return;
        this.lastSelected = null;
        this.resetState(last);
    }
    static resetState(obj) {
        if (!obj)
            return;
        this.needsUpdate = true;
    }
    static findBlockOrTextInParent(elem) {
        if (!elem)
            return null;
        if (elem.isBlock || (elem.isText)) {
            // @TODO : Replace states managements
            // if (Object.keys(elem.states).length > 0)
            return elem;
        }
        return this.findBlockOrTextInParent(elem.parent);
    }
}
//# sourceMappingURL=EventSystem.js.map