//! Translation of Flutter `LogicalKeyboardKey.keyLabel` strings to
//! Windows Virtual-Key codes (`VK_*`).
//!
//! The Flutter binding-capture dialog (`_PttBindingCaptureDialog` in
//! `apps/chanora_flutter/lib/main.dart`) emits the keyLabel into
//! `PttBinding.platform_key` (e.g. `"Space"`, `"F10"`, `"A"`,
//! `"Arrow Up"`). The bridge passes that string through unchanged.
//! On Windows the backends in `windows.rs` translate it to a
//! `VK_*` value here so RawInput and the low-level hook can match
//! against the bound key.
//!
//! Privacy: this module is the only place the platform_key string
//! is interpreted as a raw key value. The result (the integer
//! `VKEY`) **never** appears in any tracing record — it stays on
//! the stack inside the match-vs-binding hot path. The SDD-090
//! `PttSanitizer` Layer would drop a record carrying `vk` /
//! `virtual_key` / `scan_code` even if we accidentally tried to
//! log it, but we should not rely on the safety net.

use windows::Win32::UI::Input::KeyboardAndMouse as kbd;

/// Translate a Flutter `LogicalKeyboardKey.keyLabel` string into a
/// Windows Virtual-Key code. Returns `None` for labels we do not
/// understand; the caller (the binding rebind path) surfaces those
/// as `PttBackendError::InvalidBinding` so the user can pick a
/// different key.
///
/// The mapping table covers the common cases the binding-capture
/// dialog can surface on Western and East Asian keyboards. It is
/// intentionally a static lookup rather than a regex / parse:
/// platform-key strings are user-bound but Flutter-controlled, so
/// the set is small and stable per Flutter SDK.
pub fn key_label_to_vk(label: &str) -> Option<u16> {
    // Single uppercase ASCII letter — Windows VKs for A..Z match
    // their ASCII byte values (0x41..0x5A).
    if label.len() == 1 {
        let b = label.as_bytes()[0];
        if b.is_ascii_uppercase() {
            return Some(b as u16);
        }
        if b.is_ascii_lowercase() {
            return Some((b - b'a' + b'A') as u16);
        }
        if b.is_ascii_digit() {
            return Some(b as u16);
        }
    }

    Some(match label {
        // Whitespace
        "Space" | " " => kbd::VK_SPACE.0,
        "Enter" => kbd::VK_RETURN.0,
        "Tab" => kbd::VK_TAB.0,
        "Backspace" => kbd::VK_BACK.0,
        "Escape" => kbd::VK_ESCAPE.0,
        // Navigation cluster
        "Arrow Up" | "ArrowUp" => kbd::VK_UP.0,
        "Arrow Down" | "ArrowDown" => kbd::VK_DOWN.0,
        "Arrow Left" | "ArrowLeft" => kbd::VK_LEFT.0,
        "Arrow Right" | "ArrowRight" => kbd::VK_RIGHT.0,
        "Home" => kbd::VK_HOME.0,
        "End" => kbd::VK_END.0,
        "Page Up" | "PageUp" => kbd::VK_PRIOR.0,
        "Page Down" | "PageDown" => kbd::VK_NEXT.0,
        "Insert" => kbd::VK_INSERT.0,
        "Delete" => kbd::VK_DELETE.0,
        // Modifiers — Flutter exposes left/right variants too. The
        // binding-capture dialog deliberately filters bare modifier
        // presses, but a user combining modifiers might still land
        // here on the next key event.
        "Shift" | "Shift Left" | "ShiftLeft" => kbd::VK_LSHIFT.0,
        "Shift Right" | "ShiftRight" => kbd::VK_RSHIFT.0,
        "Control" | "Control Left" | "ControlLeft" => kbd::VK_LCONTROL.0,
        "Control Right" | "ControlRight" => kbd::VK_RCONTROL.0,
        "Alt" | "Alt Left" | "AltLeft" => kbd::VK_LMENU.0,
        "Alt Right" | "AltRight" => kbd::VK_RMENU.0,
        "Meta" | "Meta Left" | "MetaLeft" => kbd::VK_LWIN.0,
        "Meta Right" | "MetaRight" => kbd::VK_RWIN.0,
        "Caps Lock" | "CapsLock" => kbd::VK_CAPITAL.0,
        // F-keys
        "F1" => kbd::VK_F1.0,
        "F2" => kbd::VK_F2.0,
        "F3" => kbd::VK_F3.0,
        "F4" => kbd::VK_F4.0,
        "F5" => kbd::VK_F5.0,
        "F6" => kbd::VK_F6.0,
        "F7" => kbd::VK_F7.0,
        "F8" => kbd::VK_F8.0,
        "F9" => kbd::VK_F9.0,
        "F10" => kbd::VK_F10.0,
        "F11" => kbd::VK_F11.0,
        "F12" => kbd::VK_F12.0,
        "F13" => kbd::VK_F13.0,
        "F14" => kbd::VK_F14.0,
        "F15" => kbd::VK_F15.0,
        "F16" => kbd::VK_F16.0,
        "F17" => kbd::VK_F17.0,
        "F18" => kbd::VK_F18.0,
        "F19" => kbd::VK_F19.0,
        "F20" => kbd::VK_F20.0,
        // Punctuation / OEM keys with stable VK assignments
        ";" | "Semicolon" => kbd::VK_OEM_1.0,
        "=" | "Equal" => kbd::VK_OEM_PLUS.0,
        "," | "Comma" => kbd::VK_OEM_COMMA.0,
        "-" | "Minus" => kbd::VK_OEM_MINUS.0,
        "." | "Period" => kbd::VK_OEM_PERIOD.0,
        "/" | "Slash" => kbd::VK_OEM_2.0,
        "`" | "Backquote" => kbd::VK_OEM_3.0,
        "[" | "BracketLeft" => kbd::VK_OEM_4.0,
        "\\" | "Backslash" => kbd::VK_OEM_5.0,
        "]" | "BracketRight" => kbd::VK_OEM_6.0,
        "'" | "Quote" => kbd::VK_OEM_7.0,
        // Numpad — Flutter labels these "Numpad <n>" / "Numpad Enter" etc.
        "Numpad 0" | "Numpad0" => kbd::VK_NUMPAD0.0,
        "Numpad 1" | "Numpad1" => kbd::VK_NUMPAD1.0,
        "Numpad 2" | "Numpad2" => kbd::VK_NUMPAD2.0,
        "Numpad 3" | "Numpad3" => kbd::VK_NUMPAD3.0,
        "Numpad 4" | "Numpad4" => kbd::VK_NUMPAD4.0,
        "Numpad 5" | "Numpad5" => kbd::VK_NUMPAD5.0,
        "Numpad 6" | "Numpad6" => kbd::VK_NUMPAD6.0,
        "Numpad 7" | "Numpad7" => kbd::VK_NUMPAD7.0,
        "Numpad 8" | "Numpad8" => kbd::VK_NUMPAD8.0,
        "Numpad 9" | "Numpad9" => kbd::VK_NUMPAD9.0,
        "Numpad Enter" | "NumpadEnter" => kbd::VK_RETURN.0,
        "Numpad Multiply" | "NumpadMultiply" => kbd::VK_MULTIPLY.0,
        "Numpad Add" | "NumpadAdd" => kbd::VK_ADD.0,
        "Numpad Subtract" | "NumpadSubtract" => kbd::VK_SUBTRACT.0,
        "Numpad Decimal" | "NumpadDecimal" => kbd::VK_DECIMAL.0,
        "Numpad Divide" | "NumpadDivide" => kbd::VK_DIVIDE.0,
        "Num Lock" | "NumLock" => kbd::VK_NUMLOCK.0,
        // No match.
        _ => return None,
    })
}

/// Translate the platform-key string our binding-capture dialog
/// emits for mouse side-buttons (`"mouse-side-button:<bitmask>"`)
/// into a Windows Raw-Input mouse-button index in `[4, 5]`.
///
/// Flutter's `PointerEvent.buttons` exposes back=0x08 and
/// forward=0x10; Windows Raw Input names them XBUTTON1 and XBUTTON2.
/// Returns `Some(4)` for back / XBUTTON1, `Some(5)` for forward /
/// XBUTTON2, or `None` if the platform_key string is malformed.
pub fn mouse_side_button_index(platform_key: &str) -> Option<u8> {
    let suffix = platform_key.strip_prefix("mouse-side-button:")?;
    let bitmask: u32 = suffix.parse().ok()?;
    match bitmask {
        0x08 => Some(4),
        0x10 => Some(5),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use windows::Win32::UI::Input::KeyboardAndMouse as kbd;

    #[test]
    fn ascii_letters_map_to_vk() {
        assert_eq!(key_label_to_vk("A"), Some(0x41));
        assert_eq!(key_label_to_vk("Z"), Some(0x5A));
        // Lowercase normalised.
        assert_eq!(key_label_to_vk("a"), Some(0x41));
    }

    #[test]
    fn ascii_digits_map_to_vk() {
        assert_eq!(key_label_to_vk("0"), Some(0x30));
        assert_eq!(key_label_to_vk("9"), Some(0x39));
    }

    #[test]
    fn space_and_function_keys() {
        assert_eq!(key_label_to_vk("Space"), Some(0x20));
        assert_eq!(key_label_to_vk("F1"), Some(0x70));
        assert_eq!(key_label_to_vk("F12"), Some(0x7B));
    }

    #[test]
    fn unknown_label_returns_none() {
        assert_eq!(key_label_to_vk("Eject Disc"), None);
        assert_eq!(key_label_to_vk(""), None);
    }

    #[test]
    fn mouse_side_button_index_decodes_back_and_forward() {
        assert_eq!(mouse_side_button_index("mouse-side-button:8"), Some(4));
        assert_eq!(mouse_side_button_index("mouse-side-button:16"), Some(5));
        assert_eq!(mouse_side_button_index("keyboard"), None);
        assert_eq!(mouse_side_button_index("mouse-side-button:32"), None);
    }

    /// Comprehensive table sweep over every entry in the
    /// `key_label_to_vk` match arms. The list is intentionally
    /// verbose so a regression that drops a single key surfaces
    /// here with a precise label.
    #[test]
    fn key_label_to_vk_full_table() {
        let cases: &[(&str, u16)] = &[
            // Whitespace
            ("Space", kbd::VK_SPACE.0),
            (" ", kbd::VK_SPACE.0),
            ("Enter", kbd::VK_RETURN.0),
            ("Tab", kbd::VK_TAB.0),
            ("Backspace", kbd::VK_BACK.0),
            ("Escape", kbd::VK_ESCAPE.0),
            // Navigation
            ("Arrow Up", kbd::VK_UP.0),
            ("ArrowUp", kbd::VK_UP.0),
            ("Arrow Down", kbd::VK_DOWN.0),
            ("ArrowDown", kbd::VK_DOWN.0),
            ("Arrow Left", kbd::VK_LEFT.0),
            ("ArrowLeft", kbd::VK_LEFT.0),
            ("Arrow Right", kbd::VK_RIGHT.0),
            ("ArrowRight", kbd::VK_RIGHT.0),
            ("Home", kbd::VK_HOME.0),
            ("End", kbd::VK_END.0),
            ("Page Up", kbd::VK_PRIOR.0),
            ("PageUp", kbd::VK_PRIOR.0),
            ("Page Down", kbd::VK_NEXT.0),
            ("PageDown", kbd::VK_NEXT.0),
            ("Insert", kbd::VK_INSERT.0),
            ("Delete", kbd::VK_DELETE.0),
            // Modifiers
            ("Shift", kbd::VK_LSHIFT.0),
            ("Shift Left", kbd::VK_LSHIFT.0),
            ("ShiftLeft", kbd::VK_LSHIFT.0),
            ("Shift Right", kbd::VK_RSHIFT.0),
            ("ShiftRight", kbd::VK_RSHIFT.0),
            ("Control", kbd::VK_LCONTROL.0),
            ("Control Left", kbd::VK_LCONTROL.0),
            ("ControlLeft", kbd::VK_LCONTROL.0),
            ("Control Right", kbd::VK_RCONTROL.0),
            ("ControlRight", kbd::VK_RCONTROL.0),
            ("Alt", kbd::VK_LMENU.0),
            ("Alt Left", kbd::VK_LMENU.0),
            ("AltLeft", kbd::VK_LMENU.0),
            ("Alt Right", kbd::VK_RMENU.0),
            ("AltRight", kbd::VK_RMENU.0),
            ("Meta", kbd::VK_LWIN.0),
            ("Meta Left", kbd::VK_LWIN.0),
            ("MetaLeft", kbd::VK_LWIN.0),
            ("Meta Right", kbd::VK_RWIN.0),
            ("MetaRight", kbd::VK_RWIN.0),
            ("Caps Lock", kbd::VK_CAPITAL.0),
            ("CapsLock", kbd::VK_CAPITAL.0),
            // F1..F20
            ("F1", kbd::VK_F1.0),
            ("F2", kbd::VK_F2.0),
            ("F3", kbd::VK_F3.0),
            ("F4", kbd::VK_F4.0),
            ("F5", kbd::VK_F5.0),
            ("F6", kbd::VK_F6.0),
            ("F7", kbd::VK_F7.0),
            ("F8", kbd::VK_F8.0),
            ("F9", kbd::VK_F9.0),
            ("F10", kbd::VK_F10.0),
            ("F11", kbd::VK_F11.0),
            ("F12", kbd::VK_F12.0),
            ("F13", kbd::VK_F13.0),
            ("F14", kbd::VK_F14.0),
            ("F15", kbd::VK_F15.0),
            ("F16", kbd::VK_F16.0),
            ("F17", kbd::VK_F17.0),
            ("F18", kbd::VK_F18.0),
            ("F19", kbd::VK_F19.0),
            ("F20", kbd::VK_F20.0),
            // OEM punctuation
            (";", kbd::VK_OEM_1.0),
            ("Semicolon", kbd::VK_OEM_1.0),
            ("=", kbd::VK_OEM_PLUS.0),
            ("Equal", kbd::VK_OEM_PLUS.0),
            (",", kbd::VK_OEM_COMMA.0),
            ("Comma", kbd::VK_OEM_COMMA.0),
            ("-", kbd::VK_OEM_MINUS.0),
            ("Minus", kbd::VK_OEM_MINUS.0),
            (".", kbd::VK_OEM_PERIOD.0),
            ("Period", kbd::VK_OEM_PERIOD.0),
            ("/", kbd::VK_OEM_2.0),
            ("Slash", kbd::VK_OEM_2.0),
            ("`", kbd::VK_OEM_3.0),
            ("Backquote", kbd::VK_OEM_3.0),
            ("[", kbd::VK_OEM_4.0),
            ("BracketLeft", kbd::VK_OEM_4.0),
            ("\\", kbd::VK_OEM_5.0),
            ("Backslash", kbd::VK_OEM_5.0),
            ("]", kbd::VK_OEM_6.0),
            ("BracketRight", kbd::VK_OEM_6.0),
            ("'", kbd::VK_OEM_7.0),
            ("Quote", kbd::VK_OEM_7.0),
            // Numpad
            ("Numpad 0", kbd::VK_NUMPAD0.0),
            ("Numpad0", kbd::VK_NUMPAD0.0),
            ("Numpad 1", kbd::VK_NUMPAD1.0),
            ("Numpad1", kbd::VK_NUMPAD1.0),
            ("Numpad 2", kbd::VK_NUMPAD2.0),
            ("Numpad2", kbd::VK_NUMPAD2.0),
            ("Numpad 3", kbd::VK_NUMPAD3.0),
            ("Numpad3", kbd::VK_NUMPAD3.0),
            ("Numpad 4", kbd::VK_NUMPAD4.0),
            ("Numpad4", kbd::VK_NUMPAD4.0),
            ("Numpad 5", kbd::VK_NUMPAD5.0),
            ("Numpad5", kbd::VK_NUMPAD5.0),
            ("Numpad 6", kbd::VK_NUMPAD6.0),
            ("Numpad6", kbd::VK_NUMPAD6.0),
            ("Numpad 7", kbd::VK_NUMPAD7.0),
            ("Numpad7", kbd::VK_NUMPAD7.0),
            ("Numpad 8", kbd::VK_NUMPAD8.0),
            ("Numpad8", kbd::VK_NUMPAD8.0),
            ("Numpad 9", kbd::VK_NUMPAD9.0),
            ("Numpad9", kbd::VK_NUMPAD9.0),
            ("Numpad Enter", kbd::VK_RETURN.0),
            ("NumpadEnter", kbd::VK_RETURN.0),
            ("Numpad Multiply", kbd::VK_MULTIPLY.0),
            ("NumpadMultiply", kbd::VK_MULTIPLY.0),
            ("Numpad Add", kbd::VK_ADD.0),
            ("NumpadAdd", kbd::VK_ADD.0),
            ("Numpad Subtract", kbd::VK_SUBTRACT.0),
            ("NumpadSubtract", kbd::VK_SUBTRACT.0),
            ("Numpad Decimal", kbd::VK_DECIMAL.0),
            ("NumpadDecimal", kbd::VK_DECIMAL.0),
            ("Numpad Divide", kbd::VK_DIVIDE.0),
            ("NumpadDivide", kbd::VK_DIVIDE.0),
            ("Num Lock", kbd::VK_NUMLOCK.0),
            ("NumLock", kbd::VK_NUMLOCK.0),
        ];
        for (label, want) in cases {
            assert_eq!(
                key_label_to_vk(label),
                Some(*want),
                "key_label_to_vk({label:?}) mismatch"
            );
        }
    }

    /// All uppercase ASCII letters A..Z map to their ASCII byte
    /// value, which by Win32 convention equals the VK_* code.
    #[test]
    fn all_ascii_uppercase_letters_round_trip() {
        for c in b'A'..=b'Z' {
            let s = (c as char).to_string();
            assert_eq!(key_label_to_vk(&s), Some(c as u16), "letter {s}");
        }
    }

    /// Lowercase letters normalise to the uppercase VK_*.
    #[test]
    fn all_ascii_lowercase_letters_normalise() {
        for c in b'a'..=b'z' {
            let s = (c as char).to_string();
            let want = (c - b'a' + b'A') as u16;
            assert_eq!(key_label_to_vk(&s), Some(want), "letter {s}");
        }
    }

    /// All decimal digits 0..9 map to their ASCII byte value.
    #[test]
    fn all_digits_round_trip() {
        for c in b'0'..=b'9' {
            let s = (c as char).to_string();
            assert_eq!(key_label_to_vk(&s), Some(c as u16), "digit {s}");
        }
    }

    /// F1..F20 sweep, asserted as a contiguous range starting at
    /// VK_F1 = 0x70.
    #[test]
    fn function_keys_form_contiguous_range() {
        for n in 1u8..=20 {
            let label = format!("F{n}");
            let want = 0x70 + (n as u16 - 1);
            assert_eq!(key_label_to_vk(&label), Some(want), "function key {label}");
        }
    }

    /// Unknown / empty / whitespace labels return `None`. The
    /// caller surfaces this as `PttBackendError::InvalidBinding`.
    #[test]
    fn unknown_and_garbage_labels_return_none() {
        assert_eq!(key_label_to_vk("Banana"), None);
        assert_eq!(key_label_to_vk(""), None);
        assert_eq!(key_label_to_vk("  "), None);
        assert_eq!(key_label_to_vk("F25"), None);
        assert_eq!(key_label_to_vk("F0"), None);
        assert_eq!(key_label_to_vk("Scroll Lock"), None);
        assert_eq!(key_label_to_vk("Eject"), None);
    }

    /// Comprehensive mouse-side-button sweep including malformed
    /// suffixes.
    #[test]
    fn mouse_side_button_index_full_table() {
        assert_eq!(mouse_side_button_index("mouse-side-button:8"), Some(4));
        assert_eq!(mouse_side_button_index("mouse-side-button:16"), Some(5));
        // Unknown bitmasks.
        assert_eq!(mouse_side_button_index("mouse-side-button:99"), None);
        assert_eq!(mouse_side_button_index("mouse-side-button:0"), None);
        assert_eq!(mouse_side_button_index("mouse-side-button:1"), None);
        assert_eq!(mouse_side_button_index("mouse-side-button:2"), None);
        // Malformed suffix.
        assert_eq!(mouse_side_button_index("mouse-side-button:abc"), None);
        assert_eq!(mouse_side_button_index("mouse-side-button:"), None);
        assert_eq!(mouse_side_button_index("mouse-side-button:-1"), None);
        // No prefix.
        assert_eq!(mouse_side_button_index(""), None);
        assert_eq!(mouse_side_button_index("Space"), None);
        assert_eq!(mouse_side_button_index("keyboard"), None);
        assert_eq!(mouse_side_button_index("8"), None);
    }
}
