//! Key input primitives (port of `input/key.zig`).
//!
//! Ported: [`KeyEvent`], [`Action`], and the [`Key`] enum with its
//! `fromASCII`/`fromW3C`/`w3c`/`printable`/`modifier`/`shouldBeRemappable`/
//! `keypad`/`codepoint`/`ctrlOrSuper`/`leftOrRightShift`/`leftOrRightAlt`
//! methods, plus the module-level `ctrl_or_super` free function.
//!
//! Not ported: `imguiKey()` (lines 544-737 of the Zig source). It maps
//! `Key` variants to `cimgui` ImGui key constants for the debug UI only,
//! which has no equivalent in this headless, freestanding crate.
use crate::key_mods::Mods;

/// A generic key input event. This is the information that is necessary
/// regardless of apprt in order to generate the proper terminal
/// control sequences for a given key press.
///
/// Some apprts may not be able to provide all of this information, such
/// as GLFW. In this case, the apprt should provide as much information
/// as it can and it should be expected that the terminal behavior
/// will not be totally correct.
///
/// Port of `KeyEvent`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KeyEvent {
    /// The action: press, release, etc.
    pub action: Action,

    /// The keycode of the physical key that was pressed. This is agnostic
    /// to the layout. Layout-dependent matching can only be done via the
    /// UTF-8 or unshifted codepoint.
    pub key: Key,

    /// Mods are the modifiers that are pressed.
    pub mods: Mods,

    /// The mods that were consumed in order to generate the text
    /// in utf8. This has the mods set that were consumed, so to
    /// get the set of mods that are effective you must negate
    /// mods with this.
    ///
    /// This field is meaningless if utf8 is empty.
    pub consumed_mods: Mods,

    /// Composing is true when this key event is part of a dead key
    /// composition sequence and we're in the middle of it.
    pub composing: bool,

    /// The utf8 sequence that was generated by this key event.
    /// This will be an empty string if there is no text generated.
    /// If composing is true and this is non-empty, this is preedit
    /// text.
    ///
    /// The Zig field is `[]const u8`; we use an owned `String` here since
    /// callers construct `KeyEvent`s directly (there's no borrow-checker
    /// win to be had from `Cow`/`&str` given the value is usually short
    /// and freshly built per key event).
    pub utf8: String,

    /// The codepoint for this key when it is unshifted. For example,
    /// shift+a is "A" in UTF-8 but unshifted would provide 'a'.
    ///
    /// The Zig field is `u21`; Rust has no 21-bit integer type, so this
    /// is a `u32` with the valid range documented as `0..=0x1FFFFF`
    /// (i.e. any Unicode scalar value's codepoint, or `0` for "none").
    pub unshifted_codepoint: u32,
}

impl Default for KeyEvent {
    fn default() -> Self {
        KeyEvent {
            action: Action::Press,
            key: Key::Unidentified,
            mods: Mods::default(),
            consumed_mods: Mods::default(),
            composing: false,
            utf8: String::new(),
            unshifted_codepoint: 0,
        }
    }
}

impl KeyEvent {
    /// Returns the effective modifiers for this event. The effective
    /// modifiers are the mods that should be considered for keybindings.
    ///
    /// Port of `KeyEvent.effectiveMods`.
    pub fn effective_mods(&self) -> Mods {
        if self.utf8.is_empty() {
            return self.mods;
        }
        self.mods.unset(self.consumed_mods)
    }

    /// Returns a unique hash for this key event to be used for tracking
    /// uniqueness specifically with bindings. This omits fields that are
    /// irrelevant for bindings.
    ///
    /// The Zig version uses `std.hash.Wyhash`. We use
    /// `std::collections::hash_map::DefaultHasher` instead: its algorithm
    /// is *not* guaranteed to be stable across Rust versions or
    /// platforms, but that's fine here since binding-hash comparisons
    /// only ever happen within a single running process (e.g. comparing
    /// two hashes computed moments apart), never persisted or compared
    /// cross-process.
    ///
    /// Port of `KeyEvent.bindingHash`.
    pub fn binding_hash(&self) -> u64 {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};

        let mut hasher = DefaultHasher::new();

        // These are all the fields that are explicitly part of Trigger.
        self.key.hash(&mut hasher);
        self.unshifted_codepoint.hash(&mut hasher);
        self.mods.binding().hash(&mut hasher);

        // Notes on unmapped things and why:
        //
        // - action: we don't have action-specific bindings right now
        //   AND we want to know if a key resulted in a binding regardless
        //   of action because a press should also ignore a release and so
        //   on.
        //
        // We can add to this if there is other confusion.

        hasher.finish()
    }
}

/// The action associated with an input event.
///
/// In Zig this is backed by a `c_int` so that the enum can be used as-is
/// for the embedding API.
///
/// IMPORTANT: Any changes here update `include/ghostty.h`.
///
/// Port of `Action`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
    Release,
    Press,
    Repeat,
}

/// The set of key codes that Ghostty is aware of. These represent
/// physical keys on the keyboard. The logical key (or key string)
/// is the string that is generated by the key event and that is up
/// to the apprt to provide.
///
/// Note that these are layout-independent. For example, the "a"
/// key on a US keyboard is the same as the "ф" key on a Russian
/// keyboard, but both will report the `KeyA` enum value in the key
/// event. These values are based on the W3C standard. See:
/// <https://www.w3.org/TR/uievents-code>
///
/// Layout-dependent strings are provided in the `KeyEvent` struct as
/// UTF-8 and are produced by the associated apprt. Ghostty core has
/// no mechanism to map input events to strings without the apprt.
///
/// IMPORTANT: Any changes here update `include/ghostty.h`
/// `qwertty_term_input_key_e`.
///
/// Port of `Key`. Variant order and names are preserved from the Zig
/// source (`snake_case` -> `PascalCase`, e.g. `digit_0` -> `Digit0`,
/// `key_a` -> `KeyA`, `@"fn"` -> `Fn`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Key {
    Unidentified,

    // "Writing System Keys" § 3.1.1
    Backquote,
    Backslash,
    BracketLeft,
    BracketRight,
    Comma,
    Digit0,
    Digit1,
    Digit2,
    Digit3,
    Digit4,
    Digit5,
    Digit6,
    Digit7,
    Digit8,
    Digit9,
    Equal,
    IntlBackslash,
    IntlRo,
    IntlYen,
    KeyA,
    KeyB,
    KeyC,
    KeyD,
    KeyE,
    KeyF,
    KeyG,
    KeyH,
    KeyI,
    KeyJ,
    KeyK,
    KeyL,
    KeyM,
    KeyN,
    KeyO,
    KeyP,
    KeyQ,
    KeyR,
    KeyS,
    KeyT,
    KeyU,
    KeyV,
    KeyW,
    KeyX,
    KeyY,
    KeyZ,
    Minus,
    Period,
    Quote,
    Semicolon,
    Slash,

    // "Functional Keys" § 3.1.2
    AltLeft,
    AltRight,
    Backspace,
    CapsLock,
    ContextMenu,
    ControlLeft,
    ControlRight,
    Enter,
    MetaLeft,
    MetaRight,
    ShiftLeft,
    ShiftRight,
    Space,
    Tab,
    Convert,
    KanaMode,
    NonConvert,

    // "Control Pad Section" § 3.2
    Delete,
    End,
    Help,
    Home,
    Insert,
    PageDown,
    PageUp,

    // "Arrow Pad Section" § 3.3
    ArrowDown,
    ArrowLeft,
    ArrowRight,
    ArrowUp,

    // "Numpad Section" § 3.4
    NumLock,
    Numpad0,
    Numpad1,
    Numpad2,
    Numpad3,
    Numpad4,
    Numpad5,
    Numpad6,
    Numpad7,
    Numpad8,
    Numpad9,
    NumpadAdd,
    NumpadBackspace,
    NumpadClear,
    NumpadClearEntry,
    NumpadComma,
    NumpadDecimal,
    NumpadDivide,
    NumpadEnter,
    NumpadEqual,
    NumpadMemoryAdd,
    NumpadMemoryClear,
    NumpadMemoryRecall,
    NumpadMemoryStore,
    NumpadMemorySubtract,
    NumpadMultiply,
    NumpadParenLeft,
    NumpadParenRight,
    NumpadSubtract,

    // > For numpads that provide keys not listed here, a code value string
    // > should be created by starting with "Numpad" and appending an
    // > appropriate description of the key.
    //
    // These numpad entries are distinguished by various encoding protocols
    // (legacy and Kitty) so we support them here in case the apprt can
    // produce them.
    NumpadSeparator,
    NumpadUp,
    NumpadDown,
    NumpadRight,
    NumpadLeft,
    NumpadBegin,
    NumpadHome,
    NumpadEnd,
    NumpadInsert,
    NumpadDelete,
    NumpadPageUp,
    NumpadPageDown,

    // "Function Section" § 3.5
    Escape,
    F1,
    F2,
    F3,
    F4,
    F5,
    F6,
    F7,
    F8,
    F9,
    F10,
    F11,
    F12,
    F13,
    F14,
    F15,
    F16,
    F17,
    F18,
    F19,
    F20,
    F21,
    F22,
    F23,
    F24,
    F25,
    Fn,
    FnLock,
    PrintScreen,
    ScrollLock,
    Pause,

    // "Media Keys" § 3.6
    BrowserBack,
    BrowserFavorites,
    BrowserForward,
    BrowserHome,
    BrowserRefresh,
    BrowserSearch,
    BrowserStop,
    Eject,
    LaunchApp1,
    LaunchApp2,
    LaunchMail,
    MediaPlayPause,
    MediaSelect,
    MediaStop,
    MediaTrackNext,
    MediaTrackPrevious,
    Power,
    Sleep,
    AudioVolumeDown,
    AudioVolumeMute,
    AudioVolumeUp,
    WakeUp,

    // "Legacy, Non-standard, and Special Keys" § 3.7
    Copy,
    Cut,
    Paste,
}

/// All `Key` variants, in declaration order. Hand-written once so that
/// `fromW3C`/`w3c` round-tripping can be tested and so any future code
/// that needs to iterate all keys (e.g. building lookup tables) has a
/// single source of truth, without pulling in a derive-macro crate like
/// `strum` for a dependency-free port.
pub const ALL: &[Key] = &[
    Key::Unidentified,
    Key::Backquote,
    Key::Backslash,
    Key::BracketLeft,
    Key::BracketRight,
    Key::Comma,
    Key::Digit0,
    Key::Digit1,
    Key::Digit2,
    Key::Digit3,
    Key::Digit4,
    Key::Digit5,
    Key::Digit6,
    Key::Digit7,
    Key::Digit8,
    Key::Digit9,
    Key::Equal,
    Key::IntlBackslash,
    Key::IntlRo,
    Key::IntlYen,
    Key::KeyA,
    Key::KeyB,
    Key::KeyC,
    Key::KeyD,
    Key::KeyE,
    Key::KeyF,
    Key::KeyG,
    Key::KeyH,
    Key::KeyI,
    Key::KeyJ,
    Key::KeyK,
    Key::KeyL,
    Key::KeyM,
    Key::KeyN,
    Key::KeyO,
    Key::KeyP,
    Key::KeyQ,
    Key::KeyR,
    Key::KeyS,
    Key::KeyT,
    Key::KeyU,
    Key::KeyV,
    Key::KeyW,
    Key::KeyX,
    Key::KeyY,
    Key::KeyZ,
    Key::Minus,
    Key::Period,
    Key::Quote,
    Key::Semicolon,
    Key::Slash,
    Key::AltLeft,
    Key::AltRight,
    Key::Backspace,
    Key::CapsLock,
    Key::ContextMenu,
    Key::ControlLeft,
    Key::ControlRight,
    Key::Enter,
    Key::MetaLeft,
    Key::MetaRight,
    Key::ShiftLeft,
    Key::ShiftRight,
    Key::Space,
    Key::Tab,
    Key::Convert,
    Key::KanaMode,
    Key::NonConvert,
    Key::Delete,
    Key::End,
    Key::Help,
    Key::Home,
    Key::Insert,
    Key::PageDown,
    Key::PageUp,
    Key::ArrowDown,
    Key::ArrowLeft,
    Key::ArrowRight,
    Key::ArrowUp,
    Key::NumLock,
    Key::Numpad0,
    Key::Numpad1,
    Key::Numpad2,
    Key::Numpad3,
    Key::Numpad4,
    Key::Numpad5,
    Key::Numpad6,
    Key::Numpad7,
    Key::Numpad8,
    Key::Numpad9,
    Key::NumpadAdd,
    Key::NumpadBackspace,
    Key::NumpadClear,
    Key::NumpadClearEntry,
    Key::NumpadComma,
    Key::NumpadDecimal,
    Key::NumpadDivide,
    Key::NumpadEnter,
    Key::NumpadEqual,
    Key::NumpadMemoryAdd,
    Key::NumpadMemoryClear,
    Key::NumpadMemoryRecall,
    Key::NumpadMemoryStore,
    Key::NumpadMemorySubtract,
    Key::NumpadMultiply,
    Key::NumpadParenLeft,
    Key::NumpadParenRight,
    Key::NumpadSubtract,
    Key::NumpadSeparator,
    Key::NumpadUp,
    Key::NumpadDown,
    Key::NumpadRight,
    Key::NumpadLeft,
    Key::NumpadBegin,
    Key::NumpadHome,
    Key::NumpadEnd,
    Key::NumpadInsert,
    Key::NumpadDelete,
    Key::NumpadPageUp,
    Key::NumpadPageDown,
    Key::Escape,
    Key::F1,
    Key::F2,
    Key::F3,
    Key::F4,
    Key::F5,
    Key::F6,
    Key::F7,
    Key::F8,
    Key::F9,
    Key::F10,
    Key::F11,
    Key::F12,
    Key::F13,
    Key::F14,
    Key::F15,
    Key::F16,
    Key::F17,
    Key::F18,
    Key::F19,
    Key::F20,
    Key::F21,
    Key::F22,
    Key::F23,
    Key::F24,
    Key::F25,
    Key::Fn,
    Key::FnLock,
    Key::PrintScreen,
    Key::ScrollLock,
    Key::Pause,
    Key::BrowserBack,
    Key::BrowserFavorites,
    Key::BrowserForward,
    Key::BrowserHome,
    Key::BrowserRefresh,
    Key::BrowserSearch,
    Key::BrowserStop,
    Key::Eject,
    Key::LaunchApp1,
    Key::LaunchApp2,
    Key::LaunchMail,
    Key::MediaPlayPause,
    Key::MediaSelect,
    Key::MediaStop,
    Key::MediaTrackNext,
    Key::MediaTrackPrevious,
    Key::Power,
    Key::Sleep,
    Key::AudioVolumeDown,
    Key::AudioVolumeMute,
    Key::AudioVolumeUp,
    Key::WakeUp,
    Key::Copy,
    Key::Cut,
    Key::Paste,
];

/// The Zig `snake_case` tag name for a `Key` variant. This is the input to
/// the [`w3c`](Key::w3c) encoder and the target of the
/// [`fromW3C`](Key::fromW3C) decoder's fallback path; it exists so both
/// directions can be implemented as small runtime string transforms (see
/// the doc comments on those functions) instead of ~176 hand-written match
/// arms in each direction.
fn tag_name(key: Key) -> &'static str {
    match key {
        Key::Unidentified => "unidentified",
        Key::Backquote => "backquote",
        Key::Backslash => "backslash",
        Key::BracketLeft => "bracket_left",
        Key::BracketRight => "bracket_right",
        Key::Comma => "comma",
        Key::Digit0 => "digit_0",
        Key::Digit1 => "digit_1",
        Key::Digit2 => "digit_2",
        Key::Digit3 => "digit_3",
        Key::Digit4 => "digit_4",
        Key::Digit5 => "digit_5",
        Key::Digit6 => "digit_6",
        Key::Digit7 => "digit_7",
        Key::Digit8 => "digit_8",
        Key::Digit9 => "digit_9",
        Key::Equal => "equal",
        Key::IntlBackslash => "intl_backslash",
        Key::IntlRo => "intl_ro",
        Key::IntlYen => "intl_yen",
        Key::KeyA => "key_a",
        Key::KeyB => "key_b",
        Key::KeyC => "key_c",
        Key::KeyD => "key_d",
        Key::KeyE => "key_e",
        Key::KeyF => "key_f",
        Key::KeyG => "key_g",
        Key::KeyH => "key_h",
        Key::KeyI => "key_i",
        Key::KeyJ => "key_j",
        Key::KeyK => "key_k",
        Key::KeyL => "key_l",
        Key::KeyM => "key_m",
        Key::KeyN => "key_n",
        Key::KeyO => "key_o",
        Key::KeyP => "key_p",
        Key::KeyQ => "key_q",
        Key::KeyR => "key_r",
        Key::KeyS => "key_s",
        Key::KeyT => "key_t",
        Key::KeyU => "key_u",
        Key::KeyV => "key_v",
        Key::KeyW => "key_w",
        Key::KeyX => "key_x",
        Key::KeyY => "key_y",
        Key::KeyZ => "key_z",
        Key::Minus => "minus",
        Key::Period => "period",
        Key::Quote => "quote",
        Key::Semicolon => "semicolon",
        Key::Slash => "slash",
        Key::AltLeft => "alt_left",
        Key::AltRight => "alt_right",
        Key::Backspace => "backspace",
        Key::CapsLock => "caps_lock",
        Key::ContextMenu => "context_menu",
        Key::ControlLeft => "control_left",
        Key::ControlRight => "control_right",
        Key::Enter => "enter",
        Key::MetaLeft => "meta_left",
        Key::MetaRight => "meta_right",
        Key::ShiftLeft => "shift_left",
        Key::ShiftRight => "shift_right",
        Key::Space => "space",
        Key::Tab => "tab",
        Key::Convert => "convert",
        Key::KanaMode => "kana_mode",
        Key::NonConvert => "non_convert",
        Key::Delete => "delete",
        Key::End => "end",
        Key::Help => "help",
        Key::Home => "home",
        Key::Insert => "insert",
        Key::PageDown => "page_down",
        Key::PageUp => "page_up",
        Key::ArrowDown => "arrow_down",
        Key::ArrowLeft => "arrow_left",
        Key::ArrowRight => "arrow_right",
        Key::ArrowUp => "arrow_up",
        Key::NumLock => "num_lock",
        Key::Numpad0 => "numpad_0",
        Key::Numpad1 => "numpad_1",
        Key::Numpad2 => "numpad_2",
        Key::Numpad3 => "numpad_3",
        Key::Numpad4 => "numpad_4",
        Key::Numpad5 => "numpad_5",
        Key::Numpad6 => "numpad_6",
        Key::Numpad7 => "numpad_7",
        Key::Numpad8 => "numpad_8",
        Key::Numpad9 => "numpad_9",
        Key::NumpadAdd => "numpad_add",
        Key::NumpadBackspace => "numpad_backspace",
        Key::NumpadClear => "numpad_clear",
        Key::NumpadClearEntry => "numpad_clear_entry",
        Key::NumpadComma => "numpad_comma",
        Key::NumpadDecimal => "numpad_decimal",
        Key::NumpadDivide => "numpad_divide",
        Key::NumpadEnter => "numpad_enter",
        Key::NumpadEqual => "numpad_equal",
        Key::NumpadMemoryAdd => "numpad_memory_add",
        Key::NumpadMemoryClear => "numpad_memory_clear",
        Key::NumpadMemoryRecall => "numpad_memory_recall",
        Key::NumpadMemoryStore => "numpad_memory_store",
        Key::NumpadMemorySubtract => "numpad_memory_subtract",
        Key::NumpadMultiply => "numpad_multiply",
        Key::NumpadParenLeft => "numpad_paren_left",
        Key::NumpadParenRight => "numpad_paren_right",
        Key::NumpadSubtract => "numpad_subtract",
        Key::NumpadSeparator => "numpad_separator",
        Key::NumpadUp => "numpad_up",
        Key::NumpadDown => "numpad_down",
        Key::NumpadRight => "numpad_right",
        Key::NumpadLeft => "numpad_left",
        Key::NumpadBegin => "numpad_begin",
        Key::NumpadHome => "numpad_home",
        Key::NumpadEnd => "numpad_end",
        Key::NumpadInsert => "numpad_insert",
        Key::NumpadDelete => "numpad_delete",
        Key::NumpadPageUp => "numpad_page_up",
        Key::NumpadPageDown => "numpad_page_down",
        Key::Escape => "escape",
        Key::F1 => "f1",
        Key::F2 => "f2",
        Key::F3 => "f3",
        Key::F4 => "f4",
        Key::F5 => "f5",
        Key::F6 => "f6",
        Key::F7 => "f7",
        Key::F8 => "f8",
        Key::F9 => "f9",
        Key::F10 => "f10",
        Key::F11 => "f11",
        Key::F12 => "f12",
        Key::F13 => "f13",
        Key::F14 => "f14",
        Key::F15 => "f15",
        Key::F16 => "f16",
        Key::F17 => "f17",
        Key::F18 => "f18",
        Key::F19 => "f19",
        Key::F20 => "f20",
        Key::F21 => "f21",
        Key::F22 => "f22",
        Key::F23 => "f23",
        Key::F24 => "f24",
        Key::F25 => "f25",
        Key::Fn => "fn",
        Key::FnLock => "fn_lock",
        Key::PrintScreen => "print_screen",
        Key::ScrollLock => "scroll_lock",
        Key::Pause => "pause",
        Key::BrowserBack => "browser_back",
        Key::BrowserFavorites => "browser_favorites",
        Key::BrowserForward => "browser_forward",
        Key::BrowserHome => "browser_home",
        Key::BrowserRefresh => "browser_refresh",
        Key::BrowserSearch => "browser_search",
        Key::BrowserStop => "browser_stop",
        Key::Eject => "eject",
        Key::LaunchApp1 => "launch_app_1",
        Key::LaunchApp2 => "launch_app_2",
        Key::LaunchMail => "launch_mail",
        Key::MediaPlayPause => "media_play_pause",
        Key::MediaSelect => "media_select",
        Key::MediaStop => "media_stop",
        Key::MediaTrackNext => "media_track_next",
        Key::MediaTrackPrevious => "media_track_previous",
        Key::Power => "power",
        Key::Sleep => "sleep",
        Key::AudioVolumeDown => "audio_volume_down",
        Key::AudioVolumeMute => "audio_volume_mute",
        Key::AudioVolumeUp => "audio_volume_up",
        Key::WakeUp => "wake_up",
        Key::Copy => "copy",
        Key::Cut => "cut",
        Key::Paste => "paste",
    }
}

/// Look up a `Key` by its Zig `snake_case` tag name (the inverse of
/// [`tag_name`]). Used by [`Key::fromW3C`].
fn key_from_tag_name(name: &str) -> Option<Key> {
    ALL.iter().copied().find(|&k| tag_name(k) == name)
}

/// `(codepoint, Key)` pairs for ASCII/printable-character conversion.
///
/// Port of `Key.codepoint_map`. Keypad entries are kept LAST, matching the
/// Zig source's comment that `fromASCII` must not accidentally map plain
/// numerics/operators onto keypad keys; [`Key::from_ascii`] explicitly
/// skips entries where [`Key::keypad`] is true, and reverse lookups like
/// [`Key::codepoint`] rely on table order to prefer the first (non-keypad)
/// match.
const CODEPOINT_MAP: &[(u32, Key)] = &[
    ('a' as u32, Key::KeyA),
    ('b' as u32, Key::KeyB),
    ('c' as u32, Key::KeyC),
    ('d' as u32, Key::KeyD),
    ('e' as u32, Key::KeyE),
    ('f' as u32, Key::KeyF),
    ('g' as u32, Key::KeyG),
    ('h' as u32, Key::KeyH),
    ('i' as u32, Key::KeyI),
    ('j' as u32, Key::KeyJ),
    ('k' as u32, Key::KeyK),
    ('l' as u32, Key::KeyL),
    ('m' as u32, Key::KeyM),
    ('n' as u32, Key::KeyN),
    ('o' as u32, Key::KeyO),
    ('p' as u32, Key::KeyP),
    ('q' as u32, Key::KeyQ),
    ('r' as u32, Key::KeyR),
    ('s' as u32, Key::KeyS),
    ('t' as u32, Key::KeyT),
    ('u' as u32, Key::KeyU),
    ('v' as u32, Key::KeyV),
    ('w' as u32, Key::KeyW),
    ('x' as u32, Key::KeyX),
    ('y' as u32, Key::KeyY),
    ('z' as u32, Key::KeyZ),
    ('0' as u32, Key::Digit0),
    ('1' as u32, Key::Digit1),
    ('2' as u32, Key::Digit2),
    ('3' as u32, Key::Digit3),
    ('4' as u32, Key::Digit4),
    ('5' as u32, Key::Digit5),
    ('6' as u32, Key::Digit6),
    ('7' as u32, Key::Digit7),
    ('8' as u32, Key::Digit8),
    ('9' as u32, Key::Digit9),
    (';' as u32, Key::Semicolon),
    (' ' as u32, Key::Space),
    ('\'' as u32, Key::Quote),
    (',' as u32, Key::Comma),
    ('`' as u32, Key::Backquote),
    ('.' as u32, Key::Period),
    ('/' as u32, Key::Slash),
    ('-' as u32, Key::Minus),
    ('=' as u32, Key::Equal),
    ('[' as u32, Key::BracketLeft),
    (']' as u32, Key::BracketRight),
    ('\\' as u32, Key::Backslash),
    // Control characters
    ('\t' as u32, Key::Tab),
    // Keypad entries. We just assume keypad with the numpad_ prefix
    // so that has some special meaning. These must also always be last,
    // so that our `from_ascii` function doesn't accidentally map them
    // over normal numerics and other keys.
    ('0' as u32, Key::Numpad0),
    ('1' as u32, Key::Numpad1),
    ('2' as u32, Key::Numpad2),
    ('3' as u32, Key::Numpad3),
    ('4' as u32, Key::Numpad4),
    ('5' as u32, Key::Numpad5),
    ('6' as u32, Key::Numpad6),
    ('7' as u32, Key::Numpad7),
    ('8' as u32, Key::Numpad8),
    ('9' as u32, Key::Numpad9),
    ('.' as u32, Key::NumpadDecimal),
    ('/' as u32, Key::NumpadDivide),
    ('*' as u32, Key::NumpadMultiply),
    ('-' as u32, Key::NumpadSubtract),
    ('+' as u32, Key::NumpadAdd),
    ('=' as u32, Key::NumpadEqual),
];

impl Key {
    /// Converts an ASCII character to a key, if possible. This returns
    /// `None` if the character is unknown.
    ///
    /// Note that this can't distinguish between physical keys, i.e. '0'
    /// may be from the number row or the keypad, but it always maps
    /// to `Digit0`.
    ///
    /// This is what we want, we want people to create keybindings that
    /// are independent of the physical key.
    ///
    /// Port of `Key.fromASCII`.
    pub fn from_ascii(ch: u8) -> Option<Key> {
        for &(codepoint, key) in CODEPOINT_MAP {
            // No ASCII characters should ever map to a keypad key.
            if key.keypad() {
                continue;
            }
            if codepoint == ch as u32 {
                return Some(key);
            }
        }
        None
    }

    /// Converts a W3C key code to a Ghostty key enum value.
    ///
    /// All required W3C key codes are supported, but there are a number of
    /// non-standard key codes that are not supported. In the case the value
    /// is invalid or unsupported, this function will return `None`.
    ///
    /// Port of `Key.fromW3C`. The Zig version first tries a direct
    /// lowercase match against the tag names (this alone handles codes
    /// like `"F1"` -> `f1`, `"Enter"` -> `enter`), then falls back to
    /// converting `FooBar` to `foo_bar` (uppercase letters/digits other
    /// than the first character start a new `_`-separated segment).
    pub fn from_w3c(code: &str) -> Option<Key> {
        if code.len() > 128 {
            return None;
        }

        // First just check the whole thing lowercased, this is the simple
        // case (e.g. "Enter" -> "enter", "F1" -> "f1").
        let lower = code.to_ascii_lowercase();
        if let Some(key) = key_from_tag_name(&lower) {
            return Some(key);
        }

        // We need to convert FooBar to foo_bar.
        let mut buf = String::with_capacity(code.len() + 8);
        for (i, ch) in code.chars().enumerate() {
            match ch {
                'a'..='z' => buf.push(ch),
                // Caps and numbers trigger underscores.
                'A'..='Z' | '0'..='9' => {
                    if i > 0 {
                        buf.push('_');
                    }
                    buf.push(ch.to_ascii_lowercase());
                }
                // We don't know of any key codes that aren't alphanumeric.
                _ => return None,
            }
        }

        key_from_tag_name(&buf)
    }

    /// Converts a Ghostty key enum value to a W3C key code.
    ///
    /// Port of `Key.w3c`. The Zig version uses `inline for`/`@tagName`
    /// comptime introspection to turn the tag name into UpperCamelCase at
    /// compile time; Rust has no such reflection, so this is a small
    /// runtime string transform over [`tag_name`] instead (uppercase the
    /// first character, and the character following each `_`, dropping
    /// the `_`). This mirrors the Zig algorithm exactly and is proven to
    /// be the exact inverse of [`from_w3c`](Key::from_w3c) for every
    /// variant by the `w3c` test below.
    pub fn w3c(self) -> String {
        let name = tag_name(self);
        let mut out = String::with_capacity(name.len());
        let mut chars = name.chars().peekable();
        let mut first = true;
        while let Some(ch) = chars.next() {
            if first {
                out.extend(ch.to_uppercase());
                first = false;
            } else if ch == '_' {
                if let Some(next_ch) = chars.next() {
                    out.extend(next_ch.to_uppercase());
                }
            } else {
                out.push(ch);
            }
        }
        out
    }

    /// True if this key represents a printable character.
    ///
    /// Port of `Key.printable`.
    pub fn printable(self) -> bool {
        CODEPOINT_MAP.iter().any(|&(_, key)| key == self)
    }

    /// True if this key is a modifier.
    ///
    /// Port of `Key.modifier`.
    pub fn modifier(self) -> bool {
        matches!(
            self,
            Key::ShiftLeft
                | Key::ControlLeft
                | Key::AltLeft
                | Key::MetaLeft
                | Key::ShiftRight
                | Key::ControlRight
                | Key::AltRight
                | Key::MetaRight
        )
    }

    /// Whether this key should be remappable by the operating system.
    ///
    /// On certain OSes (namely Linux and the BSDs) certain keys like the
    /// functional keys are expected to be remappable by the user, such as
    /// in the very common use case of swapping the Caps Lock key with the
    /// Escape key with the XKB option `caps:swapescape`.
    ///
    /// However, the way XKB implements this is by essentially acting as a
    /// software key remapper that destroys all information about the
    /// original physical key, leading to very annoying bugs like #7309
    /// where the physical key `XKB_KEY_c` gets remapped into
    /// `XKB_KEY_Cyrillic_tse`, which causes all of our physical key
    /// handling to completely break down. _Very naughty._
    ///
    /// As a compromise, given that writing system keys (§3.1.1) comprise
    /// the majority of keys that "change meaning [...] based on the
    /// current locale and keyboard layout", we allow all other keys to be
    /// remapped by default since they should be fairly harmless. We might
    /// consider making this configurable, but for now this should at
    /// least placate most people.
    ///
    /// Port of `Key.shouldBeRemappable`.
    pub fn should_be_remappable(self) -> bool {
        !matches!(
            self,
            // "Writing System Keys" § 3.1.1
            Key::Backquote
                | Key::Backslash
                | Key::BracketLeft
                | Key::BracketRight
                | Key::Comma
                | Key::Digit0
                | Key::Digit1
                | Key::Digit2
                | Key::Digit3
                | Key::Digit4
                | Key::Digit5
                | Key::Digit6
                | Key::Digit7
                | Key::Digit8
                | Key::Digit9
                | Key::Equal
                | Key::IntlBackslash
                | Key::IntlRo
                | Key::IntlYen
                | Key::KeyA
                | Key::KeyB
                | Key::KeyC
                | Key::KeyD
                | Key::KeyE
                | Key::KeyF
                | Key::KeyG
                | Key::KeyH
                | Key::KeyI
                | Key::KeyJ
                | Key::KeyK
                | Key::KeyL
                | Key::KeyM
                | Key::KeyN
                | Key::KeyO
                | Key::KeyP
                | Key::KeyQ
                | Key::KeyR
                | Key::KeyS
                | Key::KeyT
                | Key::KeyU
                | Key::KeyV
                | Key::KeyW
                | Key::KeyX
                | Key::KeyY
                | Key::KeyZ
                | Key::Minus
                | Key::Period
                | Key::Quote
                | Key::Semicolon
                | Key::Slash
        )
    }

    /// Returns true if this is a keypad key.
    ///
    /// Port of `Key.keypad`.
    pub fn keypad(self) -> bool {
        tag_name(self).starts_with("numpad_")
    }

    /// Returns the codepoint representing this key, or `None` if the key
    /// is not printable.
    ///
    /// For keys with two `CODEPOINT_MAP` entries (like `Digit0`/`Numpad0`
    /// sharing `'0'`), this returns the first match in table order, i.e.
    /// the non-keypad one, since keypad entries are last in the table.
    ///
    /// Port of `Key.codepoint`.
    pub fn codepoint(self) -> Option<u32> {
        CODEPOINT_MAP
            .iter()
            .find(|&&(_, key)| key == self)
            .map(|&(codepoint, _)| codepoint)
    }

    /// True if this key is one of the left or right versions of super
    /// (macOS) or ctrl.
    ///
    /// Port of `Key.ctrlOrSuper`. The Zig version branches on
    /// `builtin.target.os.tag.isDarwin()` at comptime (so only one branch
    /// is ever compiled in). We use a runtime `cfg!(target_os = "macos")`
    /// check instead of `#[cfg(...)]`-gated functions so both branches
    /// are visible to `cargo test`/clippy on every host (the branch
    /// actually taken still depends on the host OS, matching Zig's
    /// per-platform-build behavior).
    pub fn ctrl_or_super(self) -> bool {
        if cfg!(target_os = "macos") {
            self == Key::MetaLeft || self == Key::MetaRight
        } else {
            self == Key::ControlLeft || self == Key::ControlRight
        }
    }

    /// True if this key is either left or right shift.
    ///
    /// Port of `Key.leftOrRightShift`.
    pub fn left_or_right_shift(self) -> bool {
        self == Key::ShiftLeft || self == Key::ShiftRight
    }

    /// True if this key is either left or right alt.
    ///
    /// Port of `Key.leftOrRightAlt`.
    pub fn left_or_right_alt(self) -> bool {
        self == Key::AltLeft || self == Key::AltRight
    }
}

/// This sets either "ctrl" or "super" to true (but not both) on mods
/// depending on if the build target is Mac or not. On Mac, we default to
/// super (i.e. super+c for copy) and on non-Mac we default to ctrl (i.e.
/// ctrl+c for copy).
///
/// Port of the module-level `ctrlOrSuper` free function. See
/// [`Key::ctrl_or_super`] for why this uses a runtime `cfg!()` check.
pub fn ctrl_or_super(mods: Mods) -> Mods {
    let mut copy = mods;
    if cfg!(target_os = "macos") {
        copy.super_ = true;
    } else {
        copy.ctrl = true;
    }
    copy
}

#[cfg(test)]
mod tests {
    use super::*;

    // Port of test "fromASCII should not return keypad keys"
    #[test]
    fn from_ascii_should_not_return_keypad_keys() {
        assert_eq!(Key::from_ascii(b'0'), Some(Key::Digit0));
        assert_eq!(Key::from_ascii(b'*'), None);
    }

    // Port of test "keypad keys"
    #[test]
    fn keypad_keys() {
        assert!(Key::Numpad0.keypad());
        assert!(!Key::Digit1.keypad());
    }

    // Port of test "w3c"
    #[test]
    fn w3c() {
        // All our keys should convert to and from the W3C format.
        // We don't support every key in the W3C spec, so we only
        // check the enum fields.
        for &key in ALL {
            let w3c_name = key.w3c();
            assert_eq!(Some(key), Key::from_w3c(&w3c_name));
        }
    }
}

// Port of the module-level test "ctrlOrSuper"
#[cfg(test)]
mod module_tests {
    use super::*;

    #[test]
    fn ctrl_or_super_test() {
        let m: Mods = ctrl_or_super(Mods::default());
        assert!(if cfg!(target_os = "macos") {
            m.super_
        } else {
            m.ctrl
        });
    }
}
