// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown
// SPDX-License-Identifier: GPL-3.0-or-later

package space.rcmd.android.sizzle.input

import android.view.KeyEvent
import space.rcmd.android.sizzle.model.Pitch

/**
 * Translates physical (USB / Bluetooth) keyboard events into [InputAction]s.
 * [MainActivity] forwards every hardware key here from its
 * `onKeyDown` / `onKeyUp` overrides.
 *
 * Two layers share the keyboard:
 *  - [bindings]: user-rebindable HOTKEYS (nav / transport / editing), editable
 *    from Settings → Keyboard Hotkeys. Resolved through [InputActions] plus a few
 *    keyboard-local ids ("octaveUp"/"octaveDown", and Shift-aware "nextColumn").
 *  - [pianoMap]: the fixed two-octave "tracker piano" so you can play/record
 *    notes without a MIDI device. This is the instrument layout, not a hotkey, so
 *    it is intentionally not rebindable:
 *      lower octave : Z S X D C V G B H N J M   (C .. B)
 *      upper octave : Q 2 W 3 E R 5 T 6 Y 7 U   (C .. B, one octave up)
 *
 * Hotkeys win over the piano when a key is bound to both, so the defaults avoid
 * the piano letters.
 */
class KeyboardInput(
    private val router: InputRouter,
    /** Android key-code -> action id, editable from Settings (Keyboard Hotkeys). */
    var bindings: MutableMap<Int, String> = defaultBindings(),
) {

    /** The base octave the keyboard piano plays in; adjustable with +/- keys. */
    var baseOctave: Int = 4

    // keyCode -> semitone offset from the base octave's C.
    private val pianoMap: Map<Int, Int> = mapOf(
        KeyEvent.KEYCODE_Z to 0, KeyEvent.KEYCODE_S to 1, KeyEvent.KEYCODE_X to 2,
        KeyEvent.KEYCODE_D to 3, KeyEvent.KEYCODE_C to 4, KeyEvent.KEYCODE_V to 5,
        KeyEvent.KEYCODE_G to 6, KeyEvent.KEYCODE_B to 7, KeyEvent.KEYCODE_H to 8,
        KeyEvent.KEYCODE_N to 9, KeyEvent.KEYCODE_J to 10, KeyEvent.KEYCODE_M to 11,
        KeyEvent.KEYCODE_Q to 12, KeyEvent.KEYCODE_2 to 13, KeyEvent.KEYCODE_W to 14,
        KeyEvent.KEYCODE_3 to 15, KeyEvent.KEYCODE_E to 16, KeyEvent.KEYCODE_R to 17,
        KeyEvent.KEYCODE_5 to 18, KeyEvent.KEYCODE_T to 19, KeyEvent.KEYCODE_6 to 20,
        KeyEvent.KEYCODE_Y to 21, KeyEvent.KEYCODE_7 to 22, KeyEvent.KEYCODE_U to 23,
    )

    /** @return true if we consumed the event. */
    fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        // Ignore the OS's own held-key auto-repeat (repeatCount > 0): note entry
        // sustains until release, and hotkey hold-to-repeat is driven by our own
        // accelerating repeater so it feels identical to the gamepad and MIDI.
        if (event.repeatCount > 0) return bindings.containsKey(keyCode) || pianoMap.containsKey(keyCode)

        // Keyboard-hotkey learn: capture this key for the armed action, consume it.
        if (router.learnMode && router.learnSource == InputSource.KEYBOARD) {
            router.dispatch(InputAction.RawControl(InputSource.KEYBOARD, keyCode, 1f))
            return true
        }

        bindings[keyCode]?.let { id -> return handleBoundAction(id, keyCode, event) }

        // Piano fallback for unbound keys.
        pianoMap[keyCode]?.let { offset ->
            router.dispatch(InputAction.NoteOn(pianoPitch(offset), velocity = 100))
            return true
        }
        return false
    }

    /** Run a bound hotkey. Handles the keyboard-local ids (octave nudge, and the
     *  Shift-aware column move) and delegates the rest to [InputActions]. Navigation /
     *  value-step actions start a hold-to-repeat keyed on [keyCode] (ended in
     *  [onKeyUp]); one-shots simply fire once. */
    private fun handleBoundAction(id: String, keyCode: Int, event: KeyEvent): Boolean {
        when (id) {
            "octaveDown" -> baseOctave = (baseOctave - 1).coerceIn(0, 8)
            "octaveUp" -> baseOctave = (baseOctave + 1).coerceIn(0, 8)
            // The column key doubles as prev-column when Shift is held (classic Tab).
            "nextColumn" -> router.pressRepeat(
                keyCode,
                if (event.isShiftPressed) InputAction.PrevColumn else InputAction.NextColumn,
            )
            else -> {
                val action = InputActions.fromId(id) ?: return false
                router.pressRepeat(keyCode, action)
            }
        }
        return true
    }

    fun onKeyUp(keyCode: Int, @Suppress("UNUSED_PARAMETER") event: KeyEvent): Boolean {
        // Stop any hold-to-repeat this key started (no-op for non-repeating keys).
        router.releaseRepeat(keyCode)
        if (bindings.containsKey(keyCode)) return true // consumed the bound hotkey
        val offset = pianoMap[keyCode] ?: return false
        router.dispatch(InputAction.NoteOff(pianoPitch(offset)))
        return true
    }

    private fun pianoPitch(offset: Int): Int =
        (Pitch.MIDDLE_C + (baseOctave - 4) * 12 + offset).coerceIn(Pitch.LOWEST, Pitch.HIGHEST)

    companion object {
        /** Default hotkey layout, matching the app's historical hard-coded keys.
         *  Kept off the piano letters so note entry is unaffected out of the box. */
        fun defaultBindings(): MutableMap<Int, String> = mutableMapOf(
            KeyEvent.KEYCODE_DPAD_UP to "up",
            KeyEvent.KEYCODE_DPAD_DOWN to "down",
            KeyEvent.KEYCODE_DPAD_LEFT to "left",
            KeyEvent.KEYCODE_DPAD_RIGHT to "right",
            KeyEvent.KEYCODE_TAB to "nextColumn",
            KeyEvent.KEYCODE_SPACE to "playPause",
            KeyEvent.KEYCODE_ENTER to "stop",
            KeyEvent.KEYCODE_L to "loop",
            KeyEvent.KEYCODE_PAGE_UP to "increment",
            KeyEvent.KEYCODE_PAGE_DOWN to "decrement",
            KeyEvent.KEYCODE_DEL to "clear",
            KeyEvent.KEYCODE_GRAVE to "noteOff",
            KeyEvent.KEYCODE_MINUS to "octaveDown",
            KeyEvent.KEYCODE_EQUALS to "octaveUp",
        )
    }
}
