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

package space.rcmd.android.sizzle.input

import android.view.InputDevice
import android.view.KeyEvent
import android.view.MotionEvent

/**
 * A gamepad binding trigger: a main control [code], optionally combined with a
 * held [modifier] control. [modifier] == [NONE] means "no modifier" (a plain,
 * single-control binding). This lets one control act as a "shift key": e.g. bind
 * A+Up and A+Down to different actions, and (by clearing A's own plain binding) A
 * becomes a pure modifier that does nothing on its own.
 *
 * A "control" is a button key-code, a D-pad direction (the DPAD_* key-codes, used
 * whether the pad reports as keys or as a hat), OR an analog-stick direction (the
 * synthetic `CODE_*STICK_*` codes). All three are interchangeable in chords.
 */
data class GamepadChord(val modifier: Int, val code: Int) {
    companion object {
        /** Sentinel for "no modifier". KEYCODE_UNKNOWN (0) is never a real control. */
        const val NONE = 0
    }
}

/**
 * Translates USB / Bluetooth gamepad input into [InputAction]s. Buttons arrive as
 * [KeyEvent]s; sticks, triggers and the D-pad hat arrive as [MotionEvent]s. Every
 * one is funnelled through a single press/release pipeline ([controlDown] /
 * [controlUp]) so buttons, the D-pad AND the analog sticks are all rebindable and
 * can be freely mixed into chords (e.g. "A + Left-stick-up", "L1 + D-pad-left").
 *
 * Analog directions are edge-detected with hysteresis: pushing a stick/hat past
 * [AXIS_ON] is a "press", returning inside [AXIS_OFF] is a "release".
 *
 * The map is user-rebindable ([bindings]); a sensible default layout is provided.
 */
class GamepadInput(
    private val router: InputRouter,
    var bindings: MutableMap<GamepadChord, String> = defaultBindings(),
) {
    // Current sign (-1/0/+1) of each directional axis, for edge detection. The
    // D-pad hat reuses the DPAD_* key-codes so hat- and key-reported D-pads behave
    // identically; the sticks use the synthetic CODE_*STICK_* codes.
    private var hatX = 0
    private var hatY = 0
    private var lStickX = 0
    private var lStickY = 0
    private var rStickX = 0
    private var rStickY = 0

    /** Controls physically held right now, in press order (first held = preferred
     *  modifier). Kept current by [controlDown]/[controlUp]. */
    private val heldButtons = LinkedHashSet<Int>()

    /** During gamepad-learn, the ordered controls pressed in this capture so we can
     *  reconstruct "modifier then main" once they're released. */
    private val learnSeq = ArrayList<Int>()

    fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
        if (!isGamepad(keyCode, event)) return false
        return controlDown(keyCode)
    }

    fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
        if (!isGamepad(keyCode, event)) return false
        return controlUp(keyCode)
    }

    /**
     * Handle the analog sticks and the D-pad hat. Each axis direction is
     * edge-detected and driven through the SAME press/release path as buttons, so
     * the D-pad and sticks can be bound and used in chords exactly like buttons.
     */
    fun onMotion(event: MotionEvent): Boolean {
        if (!event.isFromSource(InputDevice.SOURCE_JOYSTICK)) return false
        hatX = axis(event.getAxisValue(MotionEvent.AXIS_HAT_X), hatX, KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT)
        hatY = axis(event.getAxisValue(MotionEvent.AXIS_HAT_Y), hatY, KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN)
        lStickX = axis(event.getAxisValue(MotionEvent.AXIS_X), lStickX, CODE_LSTICK_LEFT, CODE_LSTICK_RIGHT)
        lStickY = axis(event.getAxisValue(MotionEvent.AXIS_Y), lStickY, CODE_LSTICK_UP, CODE_LSTICK_DOWN)
        rStickX = axis(event.getAxisValue(MotionEvent.AXIS_Z), rStickX, CODE_RSTICK_LEFT, CODE_RSTICK_RIGHT)
        rStickY = axis(event.getAxisValue(MotionEvent.AXIS_RZ), rStickY, CODE_RSTICK_UP, CODE_RSTICK_DOWN)
        return true
    }

    // ---- core press/release pipeline (shared by buttons, D-pad and sticks) ----

    /** @return true if the press was consumed (learning, or a bound control). */
    private fun controlDown(code: Int): Boolean {
        // Learn mode: record the press sequence and consume it (no action fires).
        if (learningGamepad()) {
            if (code !in learnSeq) learnSeq.add(code)
            heldButtons.add(code)
            return true
        }
        // Chord first: any already-held control may be the modifier. Nav / value-step
        // actions auto-repeat while held (keyed on the main control code); one-shots
        // fire once. Held sticks and D-pad hats repeat too, since they arrive here.
        for (mod in heldButtons) {
            val id = bindings[GamepadChord(mod, code)]
            if (id != null) {
                heldButtons.add(code)
                InputActions.fromId(id)?.let { router.pressRepeat(code, it) }
                return true
            }
        }
        heldButtons.add(code)
        // Otherwise the plain (no-modifier) binding.
        bindings[GamepadChord(GamepadChord.NONE, code)]?.let { id ->
            InputActions.fromId(id)?.let { router.pressRepeat(code, it); return true }
        }
        return false
    }

    private fun controlUp(code: Int): Boolean {
        heldButtons.remove(code)
        router.releaseRepeat(code) // end this control's hold-to-repeat, if any
        // Finalize a learn capture once every control in this combo is released.
        if (learningGamepad() && learnSeq.isNotEmpty() && heldButtons.none { it in learnSeq }) {
            val main = learnSeq.last()
            val mod = if (learnSeq.size >= 2) learnSeq.first() else GamepadChord.NONE
            router.dispatch(InputAction.RawControl(InputSource.GAMEPAD, main, 1f, modifier = mod))
            learnSeq.clear()
            return true
        }
        return false
    }

    /**
     * Edge-detect one analog axis with hysteresis and drive [controlDown]/
     * [controlUp] on direction changes. [neg] / [pos] are the control codes for the
     * negative / positive directions (for vertical axes, negative == up). Returns
     * the axis's new sign to store back.
     */
    private fun axis(value: Float, last: Int, neg: Int, pos: Int): Int {
        val sign = when {
            value <= -AXIS_ON -> -1
            value >= AXIS_ON -> 1
            value > -AXIS_OFF && value < AXIS_OFF -> 0
            else -> last // dead band between OFF and ON: hold to avoid chatter
        }
        if (sign != last) {
            when (last) { -1 -> controlUp(neg); 1 -> controlUp(pos) }
            when (sign) { -1 -> controlDown(neg); 1 -> controlDown(pos) }
        }
        return sign
    }

    /**
     * Whether a key event is from a gamepad. Besides the SOURCE_GAMEPAD/JOYSTICK
     * flags, we accept any gamepad-button key code ([KeyEvent.isGamepadButton]):
     * several Bluetooth controllers deliver their face/shoulder buttons with only a
     * keyboard/HID source flag set. D-pad key-codes still require a gamepad/joystick
     * source so a real keyboard's arrow keys are not captured here.
     */
    private fun isGamepad(keyCode: Int, event: KeyEvent): Boolean =
        event.isFromSource(InputDevice.SOURCE_GAMEPAD) ||
            event.isFromSource(InputDevice.SOURCE_JOYSTICK) ||
            KeyEvent.isGamepadButton(keyCode)

    private fun learningGamepad(): Boolean =
        router.learnMode && router.learnSource == InputSource.GAMEPAD

    companion object {
        private const val AXIS_ON = 0.6f   // push past this = press
        private const val AXIS_OFF = 0.4f  // return inside this = release

        // Synthetic control codes for analog-stick directions, well above the real
        // Android key-code range so they never collide with a button/D-pad code.
        const val CODE_LSTICK_LEFT = 0x20001
        const val CODE_LSTICK_RIGHT = 0x20002
        const val CODE_LSTICK_UP = 0x20003
        const val CODE_LSTICK_DOWN = 0x20004
        const val CODE_RSTICK_LEFT = 0x20005
        const val CODE_RSTICK_RIGHT = 0x20006
        const val CODE_RSTICK_UP = 0x20007
        const val CODE_RSTICK_DOWN = 0x20008

        /** Readable name for any gamepad control code (button, D-pad or stick). */
        fun controlLabel(code: Int): String = when (code) {
            CODE_LSTICK_LEFT -> "LS←"; CODE_LSTICK_RIGHT -> "LS→"
            CODE_LSTICK_UP -> "LS↑"; CODE_LSTICK_DOWN -> "LS↓"
            CODE_RSTICK_LEFT -> "RS←"; CODE_RSTICK_RIGHT -> "RS→"
            CODE_RSTICK_UP -> "RS↑"; CODE_RSTICK_DOWN -> "RS↓"
            else -> KeyEvent.keyCodeToString(code)
                .removePrefix("KEYCODE_").removePrefix("BUTTON_")
                .replace("DPAD_", "").replace('_', ' ')
        }

        /** Default Xbox/standard-layout mapping; every value is an action id. */
        fun defaultBindings(): MutableMap<GamepadChord, String> = mutableMapOf(
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_BUTTON_A) to "playPause",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_BUTTON_B) to "stop",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_BUTTON_X) to "clear",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_BUTTON_Y) to "loop",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_BUTTON_L1) to "prevTab",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_BUTTON_R1) to "nextTab",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_DPAD_UP) to "up",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_DPAD_DOWN) to "down",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_DPAD_LEFT) to "left",
            GamepadChord(GamepadChord.NONE, KeyEvent.KEYCODE_DPAD_RIGHT) to "right",
            // Left stick mirrors the D-pad; right stick edits the focused value.
            GamepadChord(GamepadChord.NONE, CODE_LSTICK_UP) to "up",
            GamepadChord(GamepadChord.NONE, CODE_LSTICK_DOWN) to "down",
            GamepadChord(GamepadChord.NONE, CODE_LSTICK_LEFT) to "left",
            GamepadChord(GamepadChord.NONE, CODE_LSTICK_RIGHT) to "right",
            GamepadChord(GamepadChord.NONE, CODE_RSTICK_UP) to "increment",
            GamepadChord(GamepadChord.NONE, CODE_RSTICK_DOWN) to "decrement",
        )
    }
}
