import Signal from "../../../core/events/signal/Signal.js";
import { KeyboardEvents } from "./events/KeyboardEvents.js";
import { InputDeviceSwitch } from "./InputDeviceSwitch.js";
import { isHTMLElementFocusable } from "./isHTMLElementFocusable.js";
import { KeyCodes } from './KeyCodes.js';

/**
 * @readonly
 * @type {string[]}
 */
const codeToKeyNameMap = [];

/**
 * Provides keyboard input.
 * Listens for key events on a DOM element and provides signals and state for key presses.
 * NOTE: when losing focus of the application, any "pressed" keys will be automatically released.
 * For example, if you hold "A" and click over into another application window - A will be forcibly released with appropriate signal.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
class KeyboardDevice {

    /**
     * @readonly
     */
    on = {
        /**
         * Fires when any key is pressed
         * @type {Signal<KeyboardEvent>}
         */
        down: new Signal(),
        /**
         * Fires when any key is released
         * @type {Signal<KeyboardEvent>}
         */
        up: new Signal()
    };

    /**
     * See {@link KeyCodes} for valid IDs
     * @readonly
     * @type {Object<InputDeviceSwitch>}
     *
     * @example
     *   const is_enter_pressed = keyboard.keys.enter.is_down;
     */
    keys = {};

    /**
     * @param {EventTarget|Element} domElement The DOM element to listen for keyboard events on.  This element *must* be focusable (e.g., have a `tabindex` attribute).
     * @throws {TypeError} If the provided `domElement` is not focusable and doesn't have a `tabindex` attribute.
     */
    constructor(domElement) {

        /*
        Only element in focus receives keyboard events, so the element supplied here must be focusable in order to be able to receive events
         */
        if (
            !isHTMLElementFocusable(domElement)
            && domElement.getAttribute('tabindex') === null
        ) {
            new TypeError('Supplied element is not inherently focusable and does not have tabindex attribute, so it must have a tabindex attribute in order to be able receive keyboard events. Something like tabindex=0 would suffice.');
        }

        /**
         * The DOM element being listened to.
         * @type {EventTarget}
         */
        this.domElement = domElement;


        //initialize separate events for each key
        for (let keyName in KeyCodes) {

            const keyCode = KeyCodes[keyName];

            codeToKeyNameMap[keyCode] = keyName;

            this.keys[keyName] = new InputDeviceSwitch();
        }

    }

    /**
     *
     * @param {KeyboardEvent} event
     * @private
     */
    #handlerKeyDown = (event) => {
        if (event.repeat) {
            // ignore automatic repetition
            // see https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat
            return;
        }

        this.on.down.send1(event);

        let should_prevent_default = false;

        //hook up dispatch handler for individual keys
        const keyCode = event.keyCode;
        const keyName = codeToKeyNameMap[keyCode];

        if (keyName !== undefined) {
            const button = this.keys[keyName];

            button.press();

            if (button.down.hasHandlers()) {
                should_prevent_default = true;
            }
        }

        if (should_prevent_default) {
            event.preventDefault();
        }
    }

    /**
     *
     * @param {KeyboardEvent} event
     * @private
     */
    #handlerKeyUp = (event) => {
        this.on.up.send1(event);

        let should_prevent_default = false;

        //hook up dispatch handler for individual keys
        const keyCode = event.keyCode;

        const keyName = codeToKeyNameMap[keyCode];

        if (keyName !== undefined) {
            const button = this.keys[keyName];

            button.release();

            if (button.down.hasHandlers()) {
                should_prevent_default = true;
            }

        }

        if (should_prevent_default) {
            event.preventDefault();
        }

    }

    /**
     *
     * @param {FocusEvent} event
     */
    #handleGlobalBlurEvent = (event) => {
        // Element lost focus, we won't be able to capture key-up events
        // release all keys
        for (let keyName in KeyCodes) {
            this.keys[keyName].release();
        }
    }

    /**
     * Starts listening for keyboard events on the associated DOM element.
     * @returns {void}
     */
    start() {
        const el = this.domElement;
        el.addEventListener(KeyboardEvents.KeyDown, this.#handlerKeyDown);
        el.addEventListener(KeyboardEvents.KeyUp, this.#handlerKeyUp);

        el.addEventListener('blur', this.#handleGlobalBlurEvent);

        //https://w3c.github.io/uievents/#event-type-focusout
        el.addEventListener('focusout', this.#handleGlobalBlurEvent);

        window.addEventListener('blur', this.#handleGlobalBlurEvent);
    }

    /**
     * Stops listening for keyboard events.
     * @returns {void}
     */
    stop() {
        const el = this.domElement;
        el.removeEventListener(KeyboardEvents.KeyDown, this.#handlerKeyDown);
        el.removeEventListener(KeyboardEvents.KeyUp, this.#handlerKeyUp);

        el.removeEventListener('blur', this.#handleGlobalBlurEvent);

        //https://w3c.github.io/uievents/#event-type-focusout
        el.removeEventListener('focusout', this.#handleGlobalBlurEvent);

        window.removeEventListener('blur', this.#handleGlobalBlurEvent);
    }

}

export default KeyboardDevice;
