import { Matrix4, Object3D, Ray, Vector2, Vector3 } from 'three';
import { showBalloonMessage, showBalloonWarning } from './debug/debug.js';
import { Context } from './engine_setup.js';
import { getTempVector, getWorldQuaternion } from './engine_three_utils.js';
import { DeviceUtilities, getParam } from './engine_utils.js';
const debug = getParam("debuginput");
/**
 * Types of pointer input devices supported by Needle Engine.
 */
export var PointerType;
(function (PointerType) {
    /** Mouse or trackpad input */
    PointerType["Mouse"] = "mouse";
    /** Touch screen input */
    PointerType["Touch"] = "touch";
    /** XR controller input (e.g., VR controllers) */
    PointerType["Controller"] = "controller";
    /** XR hand tracking input */
    PointerType["Hand"] = "hand";
})(PointerType || (PointerType = {}));
var PointerEnumType;
(function (PointerEnumType) {
    PointerEnumType["PointerDown"] = "pointerdown";
    PointerEnumType["PointerUp"] = "pointerup";
    PointerEnumType["PointerMove"] = "pointermove";
})(PointerEnumType || (PointerEnumType = {}));
var KeyboardEnumType;
(function (KeyboardEnumType) {
    KeyboardEnumType["KeyDown"] = "keydown";
    KeyboardEnumType["KeyUp"] = "keyup";
    KeyboardEnumType["KeyPressed"] = "keypress";
})(KeyboardEnumType || (KeyboardEnumType = {}));
/**
 * Event types that can be listened to via {@link Input.addEventListener}.
 * @see {@link NEPointerEvent} for pointer event data
 * @see {@link NEKeyboardEvent} for keyboard event data
 */
export var InputEvents;
(function (InputEvents) {
    /** Fired when a pointer button is pressed */
    InputEvents["PointerDown"] = "pointerdown";
    /** Fired when a pointer button is released */
    InputEvents["PointerUp"] = "pointerup";
    /** Fired when a pointer moves */
    InputEvents["PointerMove"] = "pointermove";
    /** Fired when a key is pressed down */
    InputEvents["KeyDown"] = "keydown";
    /** Fired when a key is released */
    InputEvents["KeyUp"] = "keyup";
    /** Fired when a key produces a character value */
    InputEvents["KeyPressed"] = "keypress";
})(InputEvents || (InputEvents = {}));
/**
 * Extended PointerEvent with Needle Engine-specific data.
 * Contains information about the input device, spatial data for XR, and world-space ray.
 *
 * @example Accessing event data in a component
 * ```ts
 * onPointerDown(args: PointerEventData) {
 *   const evt = args.event;
 *   console.log(`Pointer ${evt.pointerId} (${evt.pointerType})`);
 *   if (evt.isSpatial) {
 *     console.log("XR input, ray:", evt.ray);
 *   }
 * }
 * ```
 *
 * @see {@link Input} for the input management system
 * @see {@link PointerType} for available pointer types
 */
export class NEPointerEvent extends PointerEvent {
    /**
     * Spatial input data
     */
    clientZ;
    /** the device index: mouse and touch are always 0, otherwise e.g. index of the connected Gamepad or XRController */
    deviceIndex;
    /** The origin of the event contains a reference to the creator of this event.
     * This can be the Needle Engine input system or e.g. a XR controller.
     * Implement `onPointerHits` to receive the intersections of this event.
     */
    origin;
    /** the browser event that triggered this event (if any) */
    source;
    /** Is the pointer event created via a touch on screen or a spatial device like a XR controller or hand tracking? */
    mode;
    /** Returns true if the input was emitted in 3D space (and not by e.g. clicking on a 2D screen). You can use {@link mode} if you need more information about the input source */
    get isSpatial() {
        return this.mode != "screen";
    }
    /** A ray in worldspace for the event.
     * If the ray is undefined you can also use `space.worldForward` and `space.worldPosition` */
    get ray() {
        if (!this._ray) {
            this._ray = new Ray(this.space.worldPosition.clone(), this.space.worldForward.clone());
        }
        return this._ray;
    }
    set ray(value) { this._ray = value; }
    /**@returns true if this event has a ray. If you access the ray property a ray will automatically created */
    get hasRay() { return this._ray !== undefined; }
    _ray;
    /** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix)
     * E.g. you can access the input world space source position with `space.worldPosition` or world direction with `space.worldForward`
    */
    space;
    /** true if this event is a click */
    isClick = false;
    /** true if this event is a double click */
    isDoubleClick = false;
    /** @returns `true` if the event is marked to be used (when `use()` has been called). Default: `false` */
    get used() { return this._used; }
    _used = false;
    /** Call to mark an event to be used */
    use() {
        this._used = true;
    }
    /** Identifier for this pointer event.
     * For mouse and touch this is always 0.
     * For XR input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 11)
     */
    get pointerId() { return this._pointerid; }
    _pointerid;
    // this is set via the init arguments (we override it here for intellisense to show the string options)
    /** What type of input created this event: touch, mouse, xr controller, xr hand tracking... */
    get pointerType() { return this._pointerType; }
    _pointerType;
    /**
     * The button name that raised this event (e.g. for mouse events "left", "right", "middle" or for XRTrigger "xr-standard-trigger" or "xr-standard-thumbstick")
     * Use {@link button} to get the numeric button index (e.g. 0, 1, 2...) on the controller or mouse.
     */
    buttonName = undefined;
    // this is set via the init arguments (we override it here for intellisense to show the string options)
    /** The input that raised this event like `pointerdown` */
    get type() { return this._type; }
    _type;
    /** metadata can be used to associate additional information with the event */
    metadata = {};
    /** intersections that were generated from this event (or are associated with this event in any way) */
    intersections = new Array();
    constructor(type, source, init) {
        super(type, init);
        this.clientZ = init.clientZ;
        // apply the init arguments. Otherwise the arguments will be undefined in the bundled / published version of needle engine
        // so we have to be careful if we override properties - we then also need to set them in the constructor
        this._pointerid = init.pointerId;
        this._pointerType = init.pointerType;
        this._type = type;
        this.deviceIndex = init.deviceIndex;
        this.origin = init.origin;
        this.source = source;
        this.mode = init.mode;
        this._ray = init.ray;
        this.space = init.device;
        this.buttonName = init.buttonName;
    }
    _immediatePropagationStopped = false;
    get immediatePropagationStopped() {
        return this._immediatePropagationStopped;
    }
    _propagationStopped = false;
    get propagationStopped() {
        return this._immediatePropagationStopped || this._propagationStopped;
    }
    stopImmediatePropagation() {
        this._immediatePropagationStopped = true;
        super.stopImmediatePropagation();
        this.source?.stopImmediatePropagation();
    }
    stopPropagation() {
        this._propagationStopped = true;
        super.stopPropagation();
        this.source?.stopPropagation();
        if (debug)
            console.warn("Stop propagation...", this.pointerId, this.pointerType);
    }
}
export class NEKeyboardEvent extends KeyboardEvent {
    source;
    constructor(type, source, init) {
        super(type, init);
        this.source = source;
    }
    stopImmediatePropagation() {
        super.stopImmediatePropagation();
        this.source?.stopImmediatePropagation();
    }
}
export class KeyEventArgs {
    key;
    keyType;
    source;
    constructor(evt) {
        this.key = evt.key;
        this.keyType = evt.type;
        this.source = evt;
    }
}
export var InputEventQueue;
(function (InputEventQueue) {
    InputEventQueue[InputEventQueue["Early"] = -100] = "Early";
    InputEventQueue[InputEventQueue["Default"] = 0] = "Default";
    InputEventQueue[InputEventQueue["Late"] = 100] = "Late";
})(InputEventQueue || (InputEventQueue = {}));
/**
 * Handles all input events including mouse, touch, keyboard, and XR controllers.
 * Access via `this.context.input` from any component.
 *
 * @example Checking mouse/pointer state
 * ```ts
 * update() {
 *   if (this.context.input.mouseDown) {
 *     console.log("Mouse button pressed");
 *   }
 *   if (this.context.input.mouseClick) {
 *     console.log("Click detected");
 *   }
 *   const pos = this.context.input.mousePosition;
 *   console.log(`Mouse at: ${pos.x}, ${pos.y}`);
 * }
 * ```
 * @example Keyboard input
 * ```ts
 * update() {
 *   if (this.context.input.isKeyDown("Space")) {
 *     console.log("Space pressed this frame");
 *   }
 *   if (this.context.input.isKeyPressed("w")) {
 *     console.log("W key is held down");
 *   }
 * }
 * ```
 * @example Event-based input
 * ```ts
 * onEnable() {
 *   this.context.input.addEventListener("pointerdown", this.onPointerDown);
 * }
 * onDisable() {
 *   this.context.input.removeEventListener("pointerdown", this.onPointerDown);
 * }
 * onPointerDown = (evt: NEPointerEvent) => {
 *   console.log("Pointer down:", evt.pointerId);
 * }
 * ```
 *
 * @see {@link NEPointerEvent} for pointer event data
 * @see {@link InputEvents} for available event types
 * @see {@link PointerType} for pointer device types
 * @link https://engine.needle.tools/docs/scripting.html
 */
export class Input {
    /** This is a list of event listeners per event type (e.g. pointerdown, pointerup, keydown...). Each entry contains a priority and list of listeners.
     * That way users can control if they want to receive events before or after other listeners (e.g subscribe to pointer events before the EventSystem receives them) - this allows certain listeners to be always invoked first (or last) and stop propagation
     * Listeners per event are sorted
     */
    _eventListeners = {};
    addEventListener(type, callback, options) {
        if (!this._eventListeners[type])
            this._eventListeners[type] = [];
        if (!callback || typeof callback !== "function") {
            console.error("Invalid call to addEventListener: callback is required and must be a function!");
            return;
        }
        if (!options)
            options = {};
        // create a copy of the options object to avoid the original object being modified
        else
            options = { ...options };
        let queue = 0;
        if (options?.queue != undefined)
            queue = options.queue;
        const listeners = this._eventListeners[type];
        const queueListeners = listeners.find(l => l.priority === queue);
        if (!queueListeners) {
            listeners.push({ priority: queue, listeners: [{ callback, options }] });
            // ensure we sort the listeners by priority
            listeners.sort((a, b) => a.priority - b.priority);
        }
        else {
            queueListeners.listeners.push({ callback, options });
        }
    }
    removeEventListener(type, callback, options) {
        if (!this._eventListeners[type])
            return;
        if (!callback)
            return;
        const listeners = this._eventListeners[type];
        // if a specific queue is requested the callback should only be removed from that queue
        if (options?.queue != undefined) {
            const queueListeners = listeners.find(l => l.priority === options.queue);
            if (!queueListeners)
                return;
            const index = queueListeners.listeners.findIndex(l => l.callback === callback);
            if (index >= 0)
                queueListeners.listeners.splice(index, 1);
        }
        // if no queue is requested the callback will be removed from all queues 
        else {
            for (const l of listeners) {
                const index = l.listeners.findIndex(l => l.callback === callback);
                if (index >= 0)
                    l.listeners.splice(index, 1);
            }
        }
    }
    dispatchEvent(evt) {
        /** True when the next event queue should not be invoked */
        let preventNextEventQueue = false;
        // Handle keyboard event
        if (evt instanceof NEKeyboardEvent) {
            const listeners = this._eventListeners[evt.type];
            if (listeners) {
                for (const queue of listeners) {
                    for (let i = 0; i < queue.listeners.length; i++) {
                        const entry = queue.listeners[i];
                        // if the abort signal is aborted we remove the listener and will not invoke it
                        if (entry.options?.signal?.aborted) {
                            queue.listeners.splice(i, 1);
                            i--;
                            continue;
                        }
                        // if the event should only be invoked once then we remove the listener before invoking it
                        if (entry.options.once) {
                            queue.listeners.splice(i, 1);
                            i--;
                        }
                        entry.callback(evt);
                    }
                }
            }
        }
        // Hnadle pointer event
        if (evt instanceof NEPointerEvent) {
            const listeners = this._eventListeners[evt.type];
            if (listeners) {
                for (const queue of listeners) {
                    if (preventNextEventQueue)
                        break;
                    for (let i = 0; i < queue.listeners.length; i++) {
                        const entry = queue.listeners[i];
                        // if the abort signal is aborted we remove the listener and will not invoke it
                        if (entry.options?.signal?.aborted) {
                            queue.listeners.splice(i, 1);
                            i--;
                            continue;
                        }
                        // if immediatePropagationStopped is true we stop propagation altogether
                        if (evt.immediatePropagationStopped) {
                            preventNextEventQueue = true;
                            if (debug)
                                console.log("immediatePropagationStopped", evt.type);
                            break;
                        }
                        // if propagationStopped is true we continue invoking the current queue but then not invoke the next queue
                        else if (evt.propagationStopped) {
                            preventNextEventQueue = true;
                            if (debug)
                                console.log("propagationStopped", evt.type);
                            // we do not break here but continue invoking the listeners in the queue
                        }
                        // if the event should only be invoked once then we remove the listener before invoking it
                        if (entry.options.once) {
                            queue.listeners.splice(i, 1);
                            i--;
                        }
                        entry.callback(evt);
                    }
                }
            }
        }
    }
    _doubleClickTimeThreshold = .2;
    _longPressTimeThreshold = 1;
    get mousePosition() { return this._pointerPositions[0]; }
    ;
    get mousePositionRC() { return this._pointerPositionsRC[0]; }
    get mouseDown() { return this._pointerDown[0]; }
    get mouseUp() { return this._pointerUp[0]; }
    /** Is the primary pointer clicked (usually the left button). This is equivalent to `input.click` */
    get mouseClick() { return this._pointerClick[0]; }
    /** Was a double click detected for the primary pointer? This is equivalent to `input.doubleClick` */
    get mouseDoubleClick() { return this._pointerDoubleClick[0]; }
    get mousePressed() { return this._pointerPressed[0]; }
    get mouseWheelChanged() { return this.getMouseWheelChanged(0); }
    /** Is the primary pointer double clicked (usually the left button). This is equivalent to `input.mouseDoubleClick` */
    get click() { return this._pointerClick[0]; }
    /** Was a double click detected for the primary pointer? */
    get doubleClick() { return this._pointerDoubleClick[0]; }
    /**
     * Get a connected Gamepad
     * Note: For a gamepad to be available to the browser it must have received input before while the page was focused.
     * @link https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API
     * @returns The gamepad or null if no gamepad is connected
     */
    getGamepad(index = 0) {
        if (typeof navigator !== "undefined" && "getGamepads" in navigator) {
            return navigator.getGamepads()[index] || null;
        }
        return null;
    }
    _setCursorTypes = [];
    /** @deprecated use setCursor("pointer") */
    setCursorPointer() {
        this.setCursor("pointer");
    }
    /** @deprecated use unsetCursor() */
    setCursorNormal() {
        this.unsetCursor("pointer");
    }
    /**
     * Set a custom cursor. This will set the cursor type until unsetCursor is called
     */
    setCursor(type) {
        this._setCursorTypes.push(type);
        if (this._setCursorTypes.length > 10) {
            this._setCursorTypes.shift();
        }
        this.updateCursor();
    }
    /**
     * Unset a custom cursor. This will set the cursor type to the previous type or default
     */
    unsetCursor(type) {
        for (let i = this._setCursorTypes.length - 1; i >= 0; i--) {
            if (this._setCursorTypes[i] === type) {
                this._setCursorTypes.splice(i, 1);
                this.updateCursor();
                break;
            }
        }
    }
    updateCursor() {
        if (this._setCursorTypes?.length == 0)
            this.context.domElement.style.cursor = "default";
        else
            this.context.domElement.style.cursor = this._setCursorTypes[this._setCursorTypes.length - 1];
    }
    /**
     * Check if a pointer id is currently used.
     */
    getIsPointerIdInUse(pointerId) {
        for (const evt of this._pointerEventsPressed) {
            if (evt.pointerId === pointerId) {
                if (evt.used)
                    return true;
            }
        }
        return false;
    }
    /** how many pointers are currently pressed */
    getPointerPressedCount() {
        let count = 0;
        for (let i = 0; i < this._pointerPressed.length; i++) {
            if (this._pointerPressed[i]) {
                count++;
            }
        }
        return count;
    }
    /**
     * Gets the position of the given pointer index in pixel
     * @param i The pointer index
     * @returns The position of the pointer in pixel
     */
    getPointerPosition(i) {
        if (i >= this._pointerPositions.length)
            return null;
        return this._pointerPositions[i];
    }
    getPointerPositionLastFrame(i) {
        if (i >= this._pointerPositionsLastFrame.length)
            return null;
        return this._pointerPositionsLastFrame[i];
    }
    getPointerPositionDelta(i) {
        if (i >= this._pointerPositionsDelta.length)
            return null;
        return this._pointerPositionsDelta[i];
    }
    /**
     * The pointer position in screenspace coordinates (-1 to 1) where 0 is the center of the screen.
     * This can be useful for e.g. raycasting (see https://threejs.org/docs/#api/en/core/Raycaster.setFromCamera)
     */
    getPointerPositionRC(i) {
        if (i >= this._pointerPositionsRC.length)
            return null;
        return this._pointerPositionsRC[i];
    }
    getPointerDown(i) {
        if (i >= this._pointerDown.length)
            return false;
        return this._pointerDown[i];
    }
    getPointerUp(i) {
        if (i >= this._pointerUp.length)
            return false;
        return this._pointerUp[i];
    }
    getPointerPressed(i) {
        if (i >= this._pointerPressed.length)
            return false;
        const res = this._pointerPressed[i];
        // if (i === 0) console.log(...this._pointerIds);
        return res;
    }
    getPointerClicked(i) {
        if (i >= this._pointerClick.length)
            return false;
        return this._pointerClick[i];
    }
    getPointerDoubleClicked(i) {
        if (i >= this._pointerDoubleClick.length)
            return false;
        return this._pointerDoubleClick[i];
    }
    getPointerDownTime(i) {
        if (i >= this._pointerDownTime.length)
            return -1;
        return this._pointerDownTime[i];
    }
    getPointerUpTime(i) {
        if (i >= this._pointerUpTime.length)
            return -1;
        return this._pointerUpTime[i];
    }
    getPointerLongPress(i) {
        if (i >= this._pointerDownTime.length)
            return false;
        return this.getPointerPressed(i) && this.context.time.time - this._pointerDownTime[i] > this._longPressTimeThreshold;
    }
    getIsMouse(i) {
        if (i < 0 || i >= this._pointerTypes.length)
            return false;
        return this._pointerTypes[i] === PointerType.Mouse;
    }
    getIsTouch(i) {
        if (i < 0 || i >= this._pointerTypes.length)
            return false;
        return this._pointerTypes[i] === PointerType.Touch;
    }
    getTouchesPressedCount() {
        let count = 0;
        for (let i = 0; i < this._pointerPressed.length; i++) {
            if (this._pointerPressed[i] && this.getIsTouch(i)) {
                count++;
            }
        }
        return count;
    }
    getMouseWheelChanged(i = 0) {
        if (i >= this._mouseWheelChanged.length)
            return false;
        return this._mouseWheelChanged[i];
    }
    getMouseWheelDeltaY(i = 0) {
        if (i >= this._mouseWheelDeltaY.length)
            return 0;
        return this._mouseWheelDeltaY[i];
    }
    getPointerEvent(i) {
        if (i >= this._pointerEvent.length)
            return undefined;
        return this._pointerEvent[i] ?? undefined;
    }
    *foreachPointerId(pointerType) {
        for (let i = 0; i < this._pointerTypes.length; i++) {
            // check if the pointer is active
            if (this._pointerIsActive(i)) {
                // if specific pointer types are requested
                if (pointerType !== undefined) {
                    const type = this._pointerTypes[i];
                    if (Array.isArray(pointerType)) {
                        let isInArray = false;
                        for (const t of pointerType) {
                            if (type === t) {
                                isInArray = true;
                                break;
                            }
                        }
                        if (!isInArray)
                            continue;
                    }
                    else {
                        if (pointerType !== type)
                            continue;
                    }
                }
                yield i;
            }
        }
    }
    *foreachTouchId() {
        for (let i = 0; i < this._pointerTypes.length; i++) {
            const type = this._pointerTypes[i];
            if (type !== PointerType.Touch)
                continue;
            if (this._pointerIsActive[i])
                yield i;
        }
    }
    _pointerIsActive(index) {
        if (index < 0)
            return false;
        return this._pointerPressed[index] || this._pointerDown[index] || this._pointerUp[index];
    }
    context;
    _pointerDown = [false];
    _pointerUp = [false];
    _pointerClick = [false];
    _pointerDoubleClick = [false];
    _pointerPressed = [false];
    _pointerPositions = [new Vector2()];
    _pointerPositionsLastFrame = [new Vector2()];
    _pointerPositionsDelta = [new Vector2()];
    _pointerPositionsRC = [new Vector2()];
    _pointerPositionDown = [new Vector3()];
    _pointerDownTime = [];
    _pointerUpTime = [];
    _pointerUpTimestamp = [];
    _pointerIds = [];
    _pointerTypes = [""];
    _mouseWheelChanged = [false];
    _mouseWheelDeltaY = [0];
    _pointerEvent = [];
    /** current pressed pointer events. Used to check if any of those events was used  */
    _pointerEventsPressed = [];
    /** This is added/updated for pointers. screenspace pointers set this to the camera near plane  */
    _pointerSpace = [];
    _pressedStack = new Map();
    onDownButton(pointerId, button) {
        let stack = this._pressedStack.get(pointerId);
        if (!stack) {
            stack = [];
            this._pressedStack.set(pointerId, stack);
        }
        stack.push(button);
    }
    onReleaseButton(pointerId, button) {
        const stack = this._pressedStack.get(pointerId);
        if (!stack)
            return;
        const index = stack.indexOf(button);
        if (index >= 0)
            stack.splice(index, 1);
    }
    /** the first button that was down and is currently pressed */
    getFirstPressedButtonForPointer(pointerId) {
        const stack = this._pressedStack.get(pointerId);
        if (!stack)
            return undefined;
        return stack[0];
    }
    /** the last (most recent) button that was down and is currently pressed */
    getLatestPressedButtonForPointer(pointerId) {
        const stack = this._pressedStack.get(pointerId);
        if (!stack)
            return undefined;
        return stack[stack.length - 1];
    }
    getKeyDown(key) {
        // If a key is provided check if it was pressed this frame
        if (key !== undefined) {
            return this.isKeyDown(key);
        }
        // If no key was provided get the first key that was pressed this frame
        for (const key in this.keysPressed) {
            const k = this.keysPressed[key];
            if (k.startFrame === this.context.time.frameCount)
                return k.key;
        }
        return null;
    }
    getKeyPressed(key) {
        if (key !== undefined) {
            return this.isKeyPressed(key);
        }
        for (const key in this.keysPressed) {
            const k = this.keysPressed[key];
            if (k.pressed)
                return k.key;
        }
        return null;
    }
    getKeyUp(key) {
        if (key !== undefined) {
            return this.isKeyUp(key);
        }
        for (const key in this.keysPressed) {
            const k = this.keysPressed[key];
            if (k.pressed === false && k.frame === this.context.time.frameCount)
                return true;
            return false;
        }
        return null;
    }
    isKeyDown(keyCode) {
        if (!this.context.application.isVisible || !this.context.application.hasFocus)
            return false;
        const codes = this.getCodeForCommonKeyName(keyCode);
        if (codes !== null) {
            for (const code of codes)
                if (this.isKeyDown(code))
                    return true;
            return false;
        }
        const k = this.keysPressed[keyCode];
        if (!k)
            return false;
        return k.startFrame === this.context.time.frameCount && k.pressed;
    }
    isKeyUp(keyCode) {
        if (!this.context.application.isVisible || !this.context.application.hasFocus)
            return false;
        const codes = this.getCodeForCommonKeyName(keyCode);
        if (codes !== null) {
            for (const code of codes)
                if (this.isKeyUp(code))
                    return true;
            return false;
        }
        const k = this.keysPressed[keyCode];
        if (!k)
            return false;
        return k.frame === this.context.time.frameCount && k.pressed === false;
    }
    isKeyPressed(keyCode) {
        if (!this.context.application.isVisible || !this.context.application.hasFocus)
            return false;
        const codes = this.getCodeForCommonKeyName(keyCode);
        if (codes !== null) {
            for (const code of codes)
                if (this.isKeyPressed(code))
                    return true;
            return false;
        }
        const k = this.keysPressed[keyCode];
        if (!k)
            return false;
        return k.pressed || false;
    }
    // utility helper for mapping common names to actual codes; e.g. "Shift" -> "ShiftLeft" and "ShiftRight" or "a" -> "KeyA"
    getCodeForCommonKeyName(keyName) {
        if (keyName.length === 1) {
            // check if this is a digit
            if (keyName >= "0" && keyName <= "9")
                return ["Digit" + keyName];
            // check if this is a letter
            if (keyName >= "a" && keyName <= "z")
                return ["Key" + keyName.toUpperCase()];
            if (keyName == " ")
                return ["Space"];
        }
        switch (keyName) {
            case "shift":
            case "Shift":
                return ["ShiftLeft", "ShiftRight"];
            case "control":
            case "Control":
                return ["ControlLeft", "ControlRight"];
            case "alt":
            case "Alt":
                return ["AltLeft", "AltRight"];
        }
        return null;
    }
    createInputEvent(args) {
        // TODO: technically we would need to check for circular invocations here!
        switch (args.type) {
            case InputEvents.PointerDown:
                if (debug)
                    showBalloonMessage("Create Pointer down");
                this.onDownButton(args.deviceIndex, args.button);
                this.onDown(args);
                break;
            case InputEvents.PointerMove:
                if (debug)
                    showBalloonMessage("Create Pointer move");
                this.onMove(args);
                break;
            case InputEvents.PointerUp:
                if (debug)
                    showBalloonMessage("Create Pointer up");
                this.onUp(args);
                this.onReleaseButton(args.deviceIndex, args.button);
                break;
        }
    }
    convertScreenspaceToRaycastSpace(vec2) {
        vec2.x = (vec2.x - this.context.domX) / this.context.domWidth * 2 - 1;
        vec2.y = -((vec2.y - this.context.domY) / this.context.domHeight) * 2 + 1;
        return vec2;
    }
    /** @internal */
    constructor(context) {
        this.context = context;
        this.context.post_render_callbacks.push(this.onEndOfFrame);
    }
    /** this is the html element we subscribed to for events */
    _htmlEventSource;
    bindEvents() {
        this.unbindEvents();
        // we subscribe to the canvas element because we don't want to receive events when the user is interacting with the UI
        // e.g. if we have slotted HTML elements in the needle engine DOM elements we don't want to receive input events for those
        this._htmlEventSource = this.context.renderer.domElement;
        window.addEventListener('contextmenu', this.onContextMenu);
        this._htmlEventSource.addEventListener('pointerdown', this.onPointerDown, { passive: true });
        window.addEventListener('pointermove', this.onPointerMove, { passive: true, capture: true, });
        window.addEventListener('pointerup', this.onPointerUp, { passive: true });
        window.addEventListener('pointercancel', this.onPointerCancel, { passive: true });
        window.addEventListener("touchstart", this.onTouchStart, { passive: true });
        window.addEventListener("touchmove", this.onTouchMove, { passive: true });
        window.addEventListener("touchend", this.onTouchEnd, { passive: true });
        this._htmlEventSource.addEventListener('wheel', this.onMouseWheel, { passive: true });
        window.addEventListener("wheel", this.onWheelWindow, { passive: true });
        window.addEventListener("keydown", this.onKeyDown, false);
        window.addEventListener("keypress", this.onKeyPressed, false);
        window.addEventListener("keyup", this.onKeyUp, false);
        // e.g. when using sharex to capture we loose focus thus dont get e.g. key up events
        window.addEventListener('blur', this.onLostFocus);
    }
    unbindEvents() {
        for (const key in this._eventListeners) {
            this._eventListeners[key].length = 0; // clear all listeners for this event
        }
        window.removeEventListener('contextmenu', this.onContextMenu);
        this._htmlEventSource?.removeEventListener('pointerdown', this.onPointerDown);
        window.removeEventListener('pointermove', this.onPointerMove);
        window.removeEventListener('pointerup', this.onPointerUp);
        window.removeEventListener('pointercancel', this.onPointerCancel);
        window.removeEventListener("touchstart", this.onTouchStart);
        window.removeEventListener("touchmove", this.onTouchMove);
        window.removeEventListener("touchend", this.onTouchEnd);
        this._htmlEventSource?.removeEventListener('wheel', this.onMouseWheel, false);
        window.removeEventListener("wheel", this.onWheelWindow, false);
        window.removeEventListener("keydown", this.onKeyDown, false);
        window.removeEventListener("keypress", this.onKeyPressed, false);
        window.removeEventListener("keyup", this.onKeyUp, false);
        window.removeEventListener('blur', this.onLostFocus);
    }
    dispose() {
        const index = this.context.post_render_callbacks.indexOf(this.onEndOfFrame);
        if (index >= 0)
            this.context.post_render_callbacks.splice(index, 1);
        this.unbindEvents();
    }
    onLostFocus = () => {
        for (const kp in this.keysPressed) {
            this.keysPressed[kp].pressed = false;
        }
    };
    _receivedPointerMoveEventsThisFrame = new Array;
    onEndOfFrame = () => {
        this._receivedPointerMoveEventsThisFrame.length = 0;
        for (let i = 0; i < this._pointerUp.length; i++)
            this._pointerUp[i] = false;
        for (let i = 0; i < this._pointerDown.length; i++)
            this._pointerDown[i] = false;
        for (let i = 0; i < this._pointerClick.length; i++)
            this._pointerClick[i] = false;
        for (let i = 0; i < this._pointerDoubleClick.length; i++)
            this._pointerDoubleClick[i] = false;
        for (const pt of this._pointerPositionsDelta)
            pt.set(0, 0);
        for (let i = 0; i < this._mouseWheelChanged.length; i++)
            this._mouseWheelChanged[i] = false;
        for (let i = 0; i < this._mouseWheelDeltaY.length; i++)
            this._mouseWheelDeltaY[i] = 0;
    };
    canReceiveInput(evt) {
        // If the user has HTML objects ontop of the canvas
        // if(evt.target === this.context.renderer.domElement) return true;
        // const css = window.getComputedStyle(evt.target as HTMLElement);
        // if(css.pointerEvents === "all") return false;
        // We only check the target elements here since the canvas may be overlapped by other elements
        // in which case we do not want to use the input (e.g. if a HTML element is being triggered)
        if (evt.target === this.context.renderer?.domElement)
            return true;
        if (evt.target === this.context.domElement)
            return true;
        // if we are in AR we always want to receive touches because the canvas is the whole screen. 
        // See https://linear.app/needle/issue/NE-4345
        if (this.context.isInAR) {
            return true;
        }
        // looks like in Mozilla WebXR viewer the target element is the body
        if (this.context.isInAR && evt.target === document.body && DeviceUtilities.isMozillaXR())
            return true;
        if (debug)
            console.warn("CanReceiveInput:False for", evt.target);
        return false;
    }
    onContextMenu = (evt) => {
        if (this.canReceiveInput(evt) === false)
            return;
        // if (evt instanceof PointerEvent) {
        //     // for longpress on touch there might open a context menu
        //     // in which case we set the pointer pressed back to false (resetting the pressed pointer)
        //     // we need to emit a pointer up event here as well
        //     if (evt.pointerType === "touch") {
        //         // for (const index in this._pointerPressed) {
        //         //     if (this._pointerTypes[index] === PointerType.Touch) {
        //         //         // this._pointerPressed[index] = false;
        //         //         // this throws orbit controls?
        //         //         // const ne = this.createPointerEventFromTouch("pointerup", parseInt(index), this._pointerPositions[index].x, this._pointerPositions[index].y, 0, evt);
        //         //         // this.onUp(ne);
        //         //     }
        //         // }
        //     }
        // }
    };
    keysPressed = {};
    onKeyDown = (evt) => {
        if (debug)
            console.log(`key down ${evt.code}, ${this.context.application.hasFocus}`, evt);
        if (!this.context.application.hasFocus)
            return;
        const ex = this.keysPressed[evt.code];
        if (ex && ex.pressed)
            return;
        this.keysPressed[evt.code] = { pressed: true, frame: this.context.time.frameCount + 1, startFrame: this.context.time.frameCount + 1, key: evt.key, code: evt.code };
        const ne = new NEKeyboardEvent(InputEvents.KeyDown, evt, evt);
        this.onDispatchEvent(ne);
    };
    onKeyPressed = (evt) => {
        if (!this.context.application.hasFocus)
            return;
        const p = this.keysPressed[evt.code];
        if (!p)
            return;
        p.pressed = true;
        p.frame = this.context.time.frameCount + 1;
        const ne = new NEKeyboardEvent(InputEvents.KeyPressed, evt, evt);
        this.onDispatchEvent(ne);
    };
    onKeyUp = (evt) => {
        if (!this.context.application.hasFocus)
            return;
        const p = this.keysPressed[evt.code];
        if (!p)
            return;
        p.pressed = false;
        p.frame = this.context.time.frameCount + 1;
        const ne = new NEKeyboardEvent(InputEvents.KeyUp, evt, evt);
        this.onDispatchEvent(ne);
    };
    onWheelWindow = (evt) => {
        // check if we are in pointer lock mode
        if (document.pointerLockElement) {
            // only if yes we want to use the mouse wheel as a pointer event
            this.onMouseWheel(evt);
        }
    };
    onMouseWheel = (evt) => {
        if (this.canReceiveInput(evt) === false)
            return;
        if (this._mouseWheelDeltaY.length <= 0)
            this._mouseWheelDeltaY.push(0);
        if (this._mouseWheelChanged.length <= 0)
            this._mouseWheelChanged.push(false);
        this._mouseWheelChanged[0] = true;
        const current = this._mouseWheelDeltaY[0];
        this._mouseWheelDeltaY[0] = current + evt.deltaY;
    };
    onPointerDown = (evt) => {
        if (this.context.isInAR)
            return;
        if (this.canReceiveInput(evt) === false)
            return;
        if (evt.target instanceof HTMLElement) {
            evt.target.setPointerCapture(evt.pointerId);
        }
        const id = this.getPointerId(evt);
        if (debug)
            showBalloonMessage(`pointer down #${id}, identifier:${evt.pointerId}`);
        const space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
        const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: evt.button, clientX: evt.clientX, clientY: evt.clientY, pointerType: evt.pointerType, buttonName: this.getButtonName(evt), device: space, pressure: evt.pressure });
        this.onDown(ne);
    };
    onPointerMove = (evt) => {
        if (this.context.isInAR)
            return;
        // Prevent multiple pointerMove events per frame
        if (this._receivedPointerMoveEventsThisFrame.includes(evt.pointerId))
            return;
        this._receivedPointerMoveEventsThisFrame.push(evt.pointerId);
        // We want to keep receiving move events until pointerUp and not stop handling events just because we're hovering over *some* HTML element
        // if (this.canReceiveInput(evt) === false) return;
        let button = evt.button;
        if (evt.pointerType === "mouse") {
            const pressedButton = this.getFirstPressedButtonForPointer(0);
            button = pressedButton ?? 0;
        }
        const id = this.getPointerId(evt, button);
        if (button === -1) {
            button = id;
        }
        const space = this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY);
        const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: button, clientX: evt.clientX, clientY: evt.clientY, pointerType: evt.pointerType, buttonName: this.getButtonName(evt), device: space, pressure: evt.pressure });
        this.onMove(ne);
    };
    onPointerCancel = (evt) => {
        if (this.context.isInAR)
            return;
        if (debug)
            console.log("Pointer cancel", evt);
        // we treat this as an up event for now to make sure we don't have any pointers stuck in a pressed state etc. Technically we dont want to invoke a up event for cancels...
        this.onPointerUp(evt);
    };
    onPointerUp = (evt) => {
        if (this.context.isInAR)
            return;
        if (evt.target instanceof HTMLElement) {
            evt.target.releasePointerCapture(evt.pointerId);
        }
        // the pointer up event should always be handled
        // if (this.canReceiveInput(evt) === false) return;
        const id = this.getPointerId(evt);
        // if (!this.isNewEvent(evt.timeStamp, id, this._pointerUpTimestamp)) return;
        const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: evt.button, clientX: evt.clientX, clientY: evt.clientY, pointerType: evt.pointerType, buttonName: this.getButtonName(evt), device: this.getAndUpdateSpatialObjectForScreenPosition(id, evt.clientX, evt.clientY), pressure: evt.pressure });
        this.onUp(ne);
        this._pointerIds[id] = -1;
        if (debug)
            console.log("ID=" + id, "PointerId=" + evt.pointerId, "ALL:", [...this._pointerIds]);
    };
    getPointerId(evt, button) {
        if (evt.pointerType === "mouse")
            return 0 + (button ?? evt.button);
        return this.getPointerIndex(evt.pointerId);
    }
    getButtonName(evt) {
        const button = evt.button;
        if (evt.pointerType === "mouse") {
            switch (button) {
                case 0: return "left";
                case 1: return "middle";
                case 2: return "right";
            }
        }
        return "unknown";
    }
    // the touch events are currently only used for AR support on android
    onTouchStart = (evt) => {
        if (!this.context.isInAR)
            return;
        for (let i = 0; i < evt.changedTouches.length; i++) {
            const touch = evt.changedTouches[i];
            const id = this.getPointerIndex(touch.identifier);
            const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
            const ne = new NEPointerEvent(InputEvents.PointerDown, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: "touch", buttonName: "unknown", device: space, pressure: touch.force });
            this.onDown(ne);
        }
        ;
    };
    onTouchMove = (evt) => {
        if (!this.context.isInAR)
            return;
        for (let i = 0; i < evt.changedTouches.length; i++) {
            const touch = evt.changedTouches[i];
            const id = this.getPointerIndex(touch.identifier);
            const space = this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY);
            const ne = new NEPointerEvent(InputEvents.PointerMove, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: "touch", buttonName: "unknown", device: space, pressure: touch.force });
            this.onMove(ne);
        }
        ;
    };
    onTouchEnd = (evt) => {
        if (!this.context.isInAR)
            return;
        for (let i = 0; i < evt.changedTouches.length; i++) {
            const touch = evt.changedTouches[i];
            const id = this.getPointerIndex(touch.identifier);
            const ne = new NEPointerEvent(InputEvents.PointerUp, evt, { origin: this, mode: "screen", deviceIndex: 0, pointerId: id, button: 0, clientX: touch.clientX, clientY: touch.clientY, pointerType: "touch", buttonName: "unknown", device: this.getAndUpdateSpatialObjectForScreenPosition(id, touch.clientX, touch.clientY), pressure: touch.force });
            this.onUp(ne);
            this._pointerIds[id] = -1;
        }
        ;
    };
    tempNearPlaneVector = new Vector3();
    tempFarPlaneVector = new Vector3();
    tempLookMatrix = new Matrix4();
    getAndUpdateSpatialObjectForScreenPosition(id, screenX, screenY) {
        let space = this._pointerSpace[id];
        if (!space) {
            space = new Object3D();
            this._pointerSpace[id] = space;
        }
        this._pointerSpace[id] = space;
        const camera = this.context.mainCamera;
        if (camera) {
            const pointOnNearPlane = this.tempNearPlaneVector.set(screenX, screenY, -1);
            this.convertScreenspaceToRaycastSpace(pointOnNearPlane);
            const pointOnFarPlane = this.tempFarPlaneVector.set(pointOnNearPlane.x, pointOnNearPlane.y, 1);
            pointOnNearPlane.unproject(camera);
            pointOnFarPlane.unproject(camera);
            const worldUp = camera.worldUp || getTempVector(0, 1, 0).applyQuaternion(getWorldQuaternion(camera));
            this.tempLookMatrix.lookAt(pointOnFarPlane, pointOnNearPlane, worldUp);
            space.position.set(pointOnNearPlane.x, pointOnNearPlane.y, pointOnNearPlane.z);
            space.quaternion.setFromRotationMatrix(this.tempLookMatrix);
        }
        return space;
    }
    // Prevent the same event being handled twice (e.g. on touch we get a mouseUp and touchUp evt with the same timestamp)
    // private isNewEvent(timestamp: number, index: number, arr: number[]): boolean {
    //     while (arr.length <= index) arr.push(-1);
    //     if (timestamp === arr[index]) return false;
    //     arr[index] = timestamp;
    //     return true;
    // }
    isInRect(e) {
        if (this.context.isInXR)
            return true;
        const rect = this.context.domElement.getBoundingClientRect();
        const px = e.clientX;
        const py = e.clientY;
        const isInRect = px >= rect.x && px <= rect.right && py >= rect.y && py <= rect.bottom;
        if (debug && !isInRect)
            console.log("Not in rect", rect, px, py);
        return isInRect;
    }
    onDown(evt) {
        const index = evt.pointerId;
        if (this.getPointerPressed(index)) {
            // see https://linear.app/needle/issue/NE-6855#comment-3b0e3365
            if (debug)
                console.warn(`Received pointerDown event for pointerId that is already pressed: ${index}/${evt.button}`, debug ? evt : '');
            return;
        }
        if (debug)
            console.log(evt.pointerType, "DOWN", index, evt.button);
        if (!this.isInRect(evt))
            return;
        // if (this.isMouseEventFromTouch(evt)) return;
        this.setPointerState(index, this._pointerPressed, true);
        this.setPointerState(index, this._pointerDown, true);
        this.setPointerStateT(index, this._pointerEvent, evt.source);
        while (index >= this._pointerTypes.length)
            this._pointerTypes.push(evt.pointerType);
        this._pointerTypes[index] = evt.pointerType;
        while (index >= this._pointerPositionDown.length)
            this._pointerPositionDown.push(new Vector3());
        this._pointerPositionDown[index].set(evt.clientX, evt.clientY, evt.clientZ ?? 0);
        while (index >= this._pointerPositions.length)
            this._pointerPositions.push(new Vector2());
        this._pointerPositions[index].set(evt.clientX, evt.clientY);
        if (index >= this._pointerDownTime.length)
            this._pointerDownTime.push(0);
        this._pointerDownTime[index] = this.context.time.realtimeSinceStartup;
        this.updatePointerPosition(evt);
        this._pointerEventsPressed.push(evt);
        this.onDispatchEvent(evt);
    }
    // moveEvent?: Event;
    onMove(evt) {
        const index = evt.pointerId;
        const isDown = this.getPointerPressed(index);
        if (isDown === false && !this.isInRect(evt))
            return;
        if (evt.pointerType === PointerType.Touch && !isDown)
            return;
        // if (this.isMouseEventFromTouch(evt)) return;
        // if (debug) console.log(evt.pointerType, "MOVE", index, "hasSpace=" + evt.space != null);
        this.updatePointerPosition(evt);
        this.setPointerStateT(index, this._pointerEvent, evt.source);
        this.onDispatchEvent(evt);
    }
    onUp(evt) {
        const index = evt.pointerId;
        const wasDown = this.getPointerPressed(index);
        if (!wasDown) {
            if (debug)
                console.warn(`Received pointerUp for pointerId that is not pressed: ${index}/${evt.button}`, debug ? evt : '');
            return;
        }
        // if (this.isMouseEventFromTouch(evt)) return;
        if (debug)
            console.log(evt.pointerType, "UP", index);
        this.setPointerState(index, this._pointerPressed, false);
        this.setPointerStateT(index, this._pointerEvent, evt.source);
        this.setPointerState(index, this._pointerUp, true);
        this.updatePointerPosition(evt);
        for (let i = this._pointerEventsPressed.length - 1; i >= 0; i--) {
            const ptr = this._pointerEventsPressed[i];
            if (ptr.pointerId === index) {
                this._pointerEventsPressed.splice(i, 1);
                break;
            }
        }
        if (!this._pointerPositionDown[index]) {
            if (debug)
                showBalloonWarning("[Received pointer up event without matching down event for button: " + index);
            console.warn("Received pointer up event without matching down event for button: " + index);
            return;
        }
        const lastUpTime = this._pointerUpTime[index];
        const downTime = this._pointerDownTime[index];
        const upTime = this.context.time.realtimeSinceStartup;
        const dt = upTime - downTime;
        if (index >= this._pointerUpTime.length)
            this._pointerUpTime.push(-99);
        this._pointerUpTime[index] = upTime;
        // If the time from down to up is less than 1 second check if it was a click
        if (dt < 1) {
            let dx = evt.clientX - this._pointerPositionDown[index].x;
            let dy = evt.clientY - this._pointerPositionDown[index].y;
            let dz = 0;
            if (evt.isSpatial && evt.clientZ != undefined) {
                dz = evt.clientZ - this._pointerPositionDown[index].z;
                // spatial clicks remapping
                dx *= 200;
                dy *= 200;
                dz *= 200;
            }
            if (Math.abs(dx) < 5 && Math.abs(dy) < 5 && Math.abs(dz) < 5) {
                this.setPointerState(index, this._pointerClick, true);
                evt.isClick = true;
                // handle double click
                const dt = upTime - lastUpTime;
                if (debug)
                    console.log("CLICK", index, dx, dy, dz, dt);
                if (dt < this._doubleClickTimeThreshold && dt > 0) {
                    this.setPointerState(index, this._pointerDoubleClick, true);
                    evt.isDoubleClick = true;
                }
            }
        }
        this.onDispatchEvent(evt);
    }
    updatePointerPosition(evt) {
        const index = evt.pointerId;
        while (index >= this._pointerPositions.length)
            this._pointerPositions.push(new Vector2());
        while (index >= this._pointerPositionsLastFrame.length)
            this._pointerPositionsLastFrame.push(new Vector2());
        while (index >= this._pointerPositionsDelta.length)
            this._pointerPositionsDelta.push(new Vector2());
        const lf = this._pointerPositionsLastFrame[index];
        lf.copy(this._pointerPositions[index]);
        // accumulate delta (it's reset in end of frame), if we just write it here it's not correct when the browser console is open
        const delta = this._pointerPositionsDelta[index];
        let dx = evt.clientX - lf.x;
        let dy = evt.clientY - lf.y;
        // if pointer is locked, clientX and Y are not changed, but Movement is.
        if (evt.source instanceof MouseEvent || evt.source instanceof TouchEvent) {
            const source = evt.source;
            if (dx === 0 && source.movementX !== 0)
                dx = source.movementX || 0;
            if (dy === 0 && source.movementY !== 0)
                dy = source.movementY || 0;
        }
        delta.x += dx;
        delta.y += dy;
        this._pointerPositions[index].x = evt.clientX;
        this._pointerPositions[index].y = evt.clientY;
        // we want to have the position 01 on the canvas for raycasting
        const px = evt.clientX;
        const py = evt.clientY;
        while (index >= this._pointerPositionsRC.length)
            this._pointerPositionsRC.push(new Vector2());
        const rc = this._pointerPositionsRC[index];
        rc.set(px, py);
        this.convertScreenspaceToRaycastSpace(rc);
        // console.log(this.context.alias, rc);
        // this._pointerPositionsRC[evt.button].x = (px - this.context.domX) / this.context.domWidth * 2 - 1;
        // this._pointerPositionsRC[evt.button].y = -((py - this.context.domY) / this.context.domHeight) * 2 + 1;
        // console.log(evt.button)
    }
    /** get the next free id */
    getPointerIndex(pointerId) {
        // test if theres a pointer with the id
        let firstFreeIndex = -1;
        for (let i = 0; i < this._pointerIds.length; i++) {
            if (this._pointerIds[i] === pointerId)
                return i;
            else if (firstFreeIndex === -1 && this._pointerIds[i] === -1) {
                firstFreeIndex = i;
            }
        }
        // if not take the free slot if any
        if (firstFreeIndex !== -1) {
            this._pointerIds[firstFreeIndex] = pointerId;
            return firstFreeIndex;
        }
        if (debug)
            console.log("PUSH pointerId:", pointerId);
        this._pointerIds.push(pointerId);
        return this._pointerIds.length - 1;
    }
    setPointerState(index, arr, value) {
        arr[index] = value;
    }
    setPointerStateT(index, arr, value) {
        // while (arr.length <= index) arr.push(null as any);
        arr[index] = value;
        return value;
    }
    onDispatchEvent(evt) {
        const prevContext = Context.Current;
        try {
            Context.Current = this.context;
            this.dispatchEvent(evt);
        }
        finally {
            Context.Current = prevContext;
        }
    }
}
// KEY_1 = 49,
// KEY_2 = 50,
// KEY_3 = 51,
// KEY_4 = 52,
// KEY_5 = 53,
// KEY_6 = 54,
// KEY_7 = 55,
// KEY_8 = 56,
// KEY_9 = 57,
// KEY_A = 65,
// KEY_B = 66,
// KEY_C = 67,
// KEY_D = "d",
// KEY_E = 69,
// KEY_F = 70,
// KEY_G = 71,
// KEY_H = 72,
// KEY_I = 73,
// KEY_J = 74,
// KEY_K = 75,
// KEY_L = 76,
// KEY_M = 77,
// KEY_N = 78,
// KEY_O = 79,
// KEY_P = 80,
// KEY_Q = 81,
// KEY_R = 82,
// KEY_S = 83,
// KEY_T = 84,
// KEY_U = 85,
// KEY_V = 86,
// KEY_W = 87,
// KEY_X = 88,
// KEY_Y = 89,
// KEY_Z = 90,
// LEFT_META = 91,
// RIGHT_META = 92,
// SELECT = 93,
// NUMPAD_0 = 96,
// NUMPAD_1 = 97,
// NUMPAD_2 = 98,
// NUMPAD_3 = 99,
// NUMPAD_4 = 100,
// NUMPAD_5 = 101,
// NUMPAD_6 = 102,
// NUMPAD_7 = 103,
// NUMPAD_8 = 104,
// NUMPAD_9 = 105,
// MULTIPLY = 106,
// ADD = 107,
// SUBTRACT = 109,
// DECIMAL = 110,
// DIVIDE = 111,
// F1 = 112,
// F2 = 113,
// F3 = 114,
// F4 = 115,
// F5 = 116,
// F6 = 117,
// F7 = 118,
// F8 = 119,
// F9 = 120,
// F10 = 121,
// F11 = 122,
// F12 = 123,
// NUM_LOCK = 144,
// SCROLL_LOCK = 145,
// SEMICOLON = 186,
// EQUALS = 187,
// COMMA = 188,
// DASH = 189,
// PERIOD = 190,
// FORWARD_SLASH = 191,
// GRAVE_ACCENT = 192,
// OPEN_BRACKET = 219,
// BACK_SLASH = 220,
// CLOSE_BRACKET = 221,
// SINGLE_QUOTE = 222
//# sourceMappingURL=engine_input.js.map