/**
 * Ввод: клавиатура (маппинг «действие -> клавиши»), геймпад (стандартная карта)
 * и указатель (мышь/тач) в координатах виртуального разрешения.
 */

export interface PointerState {
    /** Виртуальные пиксели. */
    x: number;
    y: number;
    down: boolean;
    /** Нажато в этом кадре. */
    justPressed: boolean;
    /** Отпущено в этом кадре. */
    justReleased: boolean;
    /** true, если это событие тача (для мобильного управления). */
    isTouch: boolean;
    /** Сколько фиксированных шагов удержана (0 — только нажата). */
    downTicks: number;
}

export interface PointerEventHandlers {
    onPointerDown?: (p: PointerState) => void;
    onPointerUp?: (p: PointerState) => void;
    onPointerMove?: (p: PointerState) => void;
}

export class InputManager {
    private keysDown = new Set<string>();
    private keysPressed = new Set<string>();
    private keysReleased = new Set<string>();
    /** code -> действия (одна клавиша может маппиться на несколько действий). */
    private keyActions = new Map<string, string[]>();

    // --- геймпад (стандартная карта кнопок) ---
    /** action -> индексы кнопок стандартной карты. */
    private padActions = new Map<string, number[]>();
    private padDown = new Set<number>();
    private padPressed = new Set<number>();
    private padReleased = new Set<number>();

    private pointer: PointerState = {
        x: 0,
        y: 0,
        down: false,
        justPressed: false,
        justReleased: false,
        isTouch: false,
        downTicks: 0
    };

    private canvas: HTMLCanvasElement;
    private virtualWidth: number;
    private virtualHeight: number;
    private handlers: PointerEventHandlers;

    constructor(
        canvas: HTMLCanvasElement,
        virtualWidth: number,
        virtualHeight: number,
        handlers: PointerEventHandlers = {}
    ) {
        this.canvas = canvas;
        this.virtualWidth = virtualWidth;
        this.virtualHeight = virtualHeight;
        this.handlers = handlers;

        window.addEventListener('keydown', this.onKeyDown);
        window.addEventListener('keyup', this.onKeyUp);
        canvas.addEventListener('pointerdown', this.onPointerDown);
        canvas.addEventListener('pointerup', this.onPointerUp);
        canvas.addEventListener('pointermove', this.onPointerMove);
        canvas.addEventListener('contextmenu', this.onContextMenu);
    }

    /** Маппинг: действие -> список кодов клавиш (e.code). */
    bindActions(map: Record<string, string[]>): void {
        for (const [action, codes] of Object.entries(map)) {
            for (const code of codes) {
                const actions = this.keyActions.get(code) ?? [];
                if (!actions.includes(action)) actions.push(action);
                this.keyActions.set(code, actions);
            }
        }
    }

    /**
     * Маппинг геймпада: действие -> индексы кнопок стандартной карты
     * (0=A, 1=B, 8=Select, 9=Start, 12..15 — крестовина).
     */
    bindGamepad(map: Record<string, number[]>): void {
        for (const [action, buttons] of Object.entries(map)) {
            this.padActions.set(action, buttons);
        }
    }

    /**
     * Опрос геймпадов. Вызывается движком в начале каждого фиксированного шага
     * (до scenes.update).
     */
    update(): void {
        this.padPressed.clear();
        this.padReleased.clear();
        if (typeof navigator === 'undefined' || !navigator.getGamepads) return;
        for (const pad of navigator.getGamepads()) {
            if (!pad) continue;
            pad.buttons.forEach((btn, i) => {
                const pressed = btn.pressed;
                if (pressed && !this.padDown.has(i)) {
                    this.padPressed.add(i);
                }
                if (!pressed && this.padDown.has(i)) {
                    this.padReleased.add(i);
                }
                if (pressed) {
                    this.padDown.add(i);
                } else {
                    this.padDown.delete(i);
                }
            });
            break; // опрашиваем первый подключённый геймпад
        }
    }

    /** Оси левого стика (-1..1) первого подключённого геймпада (или null). */
    getLeftStick(): { x: number; y: number } | null {
        if (typeof navigator === 'undefined' || !navigator.getGamepads) return null;
        for (const pad of navigator.getGamepads()) {
            if (!pad) continue;
            const [x = 0, y = 0] = pad.axes;
            return {
                x: Math.abs(x) < 0.15 ? 0 : x, // мёртвая зона
                y: Math.abs(y) < 0.15 ? 0 : y
            };
        }
        return null;
    }

    /** Действие активно (зажато): клавиатура или геймпад. */
    isActionActive(action: string): boolean {
        for (const [code, actions] of this.keyActions) {
            if (actions.includes(action) && this.keysDown.has(code)) return true;
        }
        const padButtons = this.padActions.get(action);
        if (padButtons) {
            for (const b of padButtons) {
                if (this.padDown.has(b)) return true;
            }
        }
        return false;
    }

    /** Действие нажато именно в этом тике. */
    isActionJustPressed(action: string): boolean {
        for (const code of this.keysPressed) {
            if (this.keyActions.get(code)?.includes(action)) return true;
        }
        const padButtons = this.padActions.get(action);
        if (padButtons) {
            for (const b of padButtons) {
                if (this.padPressed.has(b)) return true;
            }
        }
        return false;
    }

    /** Действие отпущено именно в этом тике (клавиатура или геймпад). */
    isActionJustReleased(action: string): boolean {
        for (const code of this.keysReleased) {
            if (this.keyActions.get(code)?.includes(action)) return true;
        }
        const padButtons = this.padActions.get(action);
        if (padButtons) {
            for (const b of padButtons) {
                if (this.padReleased.has(b)) return true;
            }
        }
        return false;
    }

    isKeyDown(code: string): boolean {
        return this.keysDown.has(code);
    }

    wasKeyPressed(code: string): boolean {
        return this.keysPressed.has(code);
    }

    wasKeyReleased(code: string): boolean {
        return this.keysReleased.has(code);
    }

    getPointer(): Readonly<PointerState> {
        return this.pointer;
    }

    /** Вызывается в конце каждого тика: очищает «just pressed/released», копит downTicks. */
    endTick(): void {
        this.keysPressed.clear();
        this.keysReleased.clear();
        this.padPressed.clear();
        this.padReleased.clear();
        this.pointer.justPressed = false;
        this.pointer.justReleased = false;
        if (this.pointer.down) {
            this.pointer.downTicks += 1;
        } else {
            this.pointer.downTicks = 0;
        }
    }

    dispose(): void {
        window.removeEventListener('keydown', this.onKeyDown);
        window.removeEventListener('keyup', this.onKeyUp);
        this.canvas.removeEventListener('pointerdown', this.onPointerDown);
        this.canvas.removeEventListener('pointerup', this.onPointerUp);
        this.canvas.removeEventListener('pointermove', this.onPointerMove);
        this.canvas.removeEventListener('contextmenu', this.onContextMenu);
    }

    // --- инъекция ввода (агентный мост / тесты) ---
    // Идёт через ТЕ ЖЕ структуры, что и реальные события: инъекция между тиками
    // семантически идентична нажатию, «just pressed» виден ровно один тик.

    /** pointerdown в виртуальных пикселях (без DOM). */
    injectPointerDown(x: number, y: number, isTouch = false): void {
        this.pressPointer(x, y, isTouch);
    }

    /** pointerup в виртуальных пикселях (без DOM). */
    injectPointerUp(x: number, y: number, isTouch = false): void {
        this.releasePointer(x, y, isTouch);
    }

    /** Действие «нажато» (isActionJustPressed на следующем тике). */
    injectAction(action: string): void {
        for (const code of this.codesOf(action)) this.pressCode(code);
    }

    /** Действие «отпущено». */
    injectActionRelease(action: string): void {
        for (const code of this.codesOf(action)) this.releaseCode(code);
    }

    /** Сырой код клавиши (e.code) — нажать и отпустить нельзя по отдельности: см. injectKeyCodeUp. */
    injectKeyCode(code: string): void {
        this.pressCode(code);
    }

    /** Сырой код клавиши (e.code) — отпустить. */
    injectKeyCodeUp(code: string): void {
        this.releaseCode(code);
    }

    // --- общие примитивы (DOM-события и инъекции идут через одни и те же) ---

    private pressCode(code: string): void {
        if (!this.keysDown.has(code)) this.keysPressed.add(code);
        this.keysDown.add(code);
    }

    private releaseCode(code: string): void {
        this.keysDown.delete(code);
        this.keysReleased.add(code);
    }

    /** Все коды клавиш, замапленные на действие. */
    private codesOf(action: string): string[] {
        const out: string[] = [];
        for (const [code, actions] of this.keyActions) {
            if (actions.includes(action)) out.push(code);
        }
        return out;
    }

    private pressPointer(x: number, y: number, isTouch: boolean): void {
        this.pointer.x = x;
        this.pointer.y = y;
        this.pointer.down = true;
        this.pointer.justPressed = true;
        this.pointer.downTicks = 0;
        this.pointer.isTouch = isTouch;
        this.handlers.onPointerDown?.(this.pointer);
    }

    private releasePointer(x: number, y: number, isTouch: boolean): void {
        this.pointer.x = x;
        this.pointer.y = y;
        this.pointer.down = false;
        this.pointer.justReleased = true;
        this.pointer.isTouch = isTouch;
        this.handlers.onPointerUp?.(this.pointer);
    }

    private onKeyDown = (e: KeyboardEvent): void => {
        this.pressCode(e.code);
    };

    private onKeyUp = (e: KeyboardEvent): void => {
        this.releaseCode(e.code);
    };

    private toVirtual(e: PointerEvent): { x: number; y: number } {
        const rect = this.canvas.getBoundingClientRect();
        return {
            x: ((e.clientX - rect.left) / rect.width) * this.virtualWidth,
            y: ((e.clientY - rect.top) / rect.height) * this.virtualHeight
        };
    }

    private onPointerDown = (e: PointerEvent): void => {
        const { x, y } = this.toVirtual(e);
        this.pressPointer(x, y, e.pointerType === 'touch');
    };

    private onPointerUp = (e: PointerEvent): void => {
        const { x, y } = this.toVirtual(e);
        this.releasePointer(x, y, e.pointerType === 'touch');
    };

    private onPointerMove = (e: PointerEvent): void => {
        const { x, y } = this.toVirtual(e);
        this.pointer.x = x;
        this.pointer.y = y;
        this.handlers.onPointerMove?.(this.pointer);
    };

    private onContextMenu = (e: Event): void => {
        e.preventDefault();
    };
}