const std = @import("std");
const Allocator = std.mem.Allocator;

/// 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.
pub const KeyEvent = struct {
    /// The action: press, release, etc.
    action: Action = .press,

    /// "key" is the logical key that was pressed. For example, if
    /// a Dvorak keyboard layout is being used on a US keyboard,
    /// the "i" physical key will be reported as "c". The physical
    /// key is the key that was physically pressed on the keyboard.
    key: Key,
    physical_key: Key = .invalid,

    /// Mods are the modifiers that are pressed.
    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.
    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.
    composing: bool = false,

    /// 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.
    utf8: []const u8 = "",

    /// The codepoint for this key when it is unshifted. For example,
    /// shift+a is "A" in UTF-8 but unshifted would provide 'a'.
    unshifted_codepoint: u21 = 0,

    /// Returns the effective modifiers for this event. The effective
    /// modifiers are the mods that should be considered for keybindings.
    pub fn effectiveMods(self: KeyEvent) Mods {
        if (self.utf8.len == 0) return self.mods;
        return self.mods.unset(self.consumed_mods);
    }
};

/// A bitmask for all key modifiers.
///
/// IMPORTANT: Any changes here update include/ghostty.h
pub const Mods = packed struct(Mods.Backing) {
    pub const Backing = u16;

    shift: bool = false,
    ctrl: bool = false,
    alt: bool = false,
    super: bool = false,
    caps_lock: bool = false,
    num_lock: bool = false,
    sides: side = .{},
    _padding: u6 = 0,

    /// Tracks the side that is active for any given modifier. Note
    /// that this doesn't confirm a modifier is pressed; you must check
    /// the bool for that in addition to this.
    ///
    /// Not all platforms support this, check apprt for more info.
    pub const side = packed struct(u4) {
        shift: Side = .left,
        ctrl: Side = .left,
        alt: Side = .left,
        super: Side = .left,
    };

    pub const Side = enum { left, right };

    /// Integer value of this struct.
    pub fn int(self: Mods) Backing {
        return @bitCast(self);
    }

    /// Returns true if no modifiers are set.
    pub fn empty(self: Mods) bool {
        return self.int() == 0;
    }

    /// Returns true if two mods are equal.
    pub fn equal(self: Mods, other: Mods) bool {
        return self.int() == other.int();
    }

    /// Return mods that are only relevant for bindings.
    pub fn binding(self: Mods) Mods {
        return .{
            .shift = self.shift,
            .ctrl = self.ctrl,
            .alt = self.alt,
            .super = self.super,
        };
    }

    /// Perform `self &~ other` to remove the other mods from self.
    pub fn unset(self: Mods, other: Mods) Mods {
        return @bitCast(self.int() & ~other.int());
    }

    /// Returns the mods without locks set.
    pub fn withoutLocks(self: Mods) Mods {
        var copy = self;
        copy.caps_lock = false;
        copy.num_lock = false;
        return copy;
    }

    // For our own understanding
    test {
        const testing = std.testing;
        try testing.expectEqual(@as(Backing, @bitCast(Mods{})), @as(Backing, 0b0));
        try testing.expectEqual(
            @as(Backing, @bitCast(Mods{ .shift = true })),
            @as(Backing, 0b0000_0001),
        );
    }
};

/// The action associated with an input event. This is backed by a c_int
/// so that we can use the enum as-is for our embedding API.
///
/// IMPORTANT: Any changes here update include/ghostty.h
pub const Action = enum(c_int) {
    release,
    press,
    repeat,
};

/// The set of keys that can map to keybindings. These have no fixed enum
/// values because we map platform-specific keys to this set. Note that
/// this only needs to accommodate what maps to a key. If a key is not bound
/// to anything and the key can be mapped to a printable character, then that
/// unicode character is sent directly to the pty.
///
/// This is backed by a c_int so we can use this as-is for our embedding API.
///
/// IMPORTANT: Any changes here update include/ghostty.h
pub const Key = enum(c_int) {
    invalid,

    // a-z
    a,
    b,
    c,
    d,
    e,
    f,
    g,
    h,
    i,
    j,
    k,
    l,
    m,
    n,
    o,
    p,
    q,
    r,
    s,
    t,
    u,
    v,
    w,
    x,
    y,
    z,

    // numbers
    zero,
    one,
    two,
    three,
    four,
    five,
    six,
    seven,
    eight,
    nine,

    // puncuation
    semicolon,
    space,
    apostrophe,
    comma,
    grave_accent, // `
    period,
    slash,
    minus,
    equal,
    left_bracket, // [
    right_bracket, // ]
    backslash, // /

    // control
    up,
    down,
    right,
    left,
    home,
    end,
    insert,
    delete,
    caps_lock,
    scroll_lock,
    num_lock,
    page_up,
    page_down,
    escape,
    enter,
    tab,
    backspace,
    print_screen,
    pause,

    // function keys
    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,

    // keypad
    kp_0,
    kp_1,
    kp_2,
    kp_3,
    kp_4,
    kp_5,
    kp_6,
    kp_7,
    kp_8,
    kp_9,
    kp_decimal,
    kp_divide,
    kp_multiply,
    kp_subtract,
    kp_add,
    kp_enter,
    kp_equal,

    // modifiers
    left_shift,
    left_control,
    left_alt,
    left_super,
    right_shift,
    right_control,
    right_alt,
    right_super,

    // To support more keys (there are obviously more!) add them here
    // and ensure the mapping is up to date in the Window key handler.

    /// Converts an ASCII character to a key, if possible. This returns
    /// null 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 '.zero'.
    ///
    /// This is what we want, we awnt people to create keybindings that
    /// are independent of the physical key.
    pub fn fromASCII(ch: u8) ?Key {
        return switch (ch) {
            inline else => |comptime_ch| {
                return comptime result: {
                    @setEvalBranchQuota(100_000);
                    for (codepoint_map) |entry| {
                        // No ASCII characters should ever map to a keypad key
                        if (entry[1].keypad()) continue;

                        if (entry[0] == @as(u21, @intCast(comptime_ch))) {
                            break :result entry[1];
                        }
                    }

                    break :result null;
                };
            },
        };
    }

    /// True if this key represents a printable character.
    pub fn printable(self: Key) bool {
        return switch (self) {
            inline else => |tag| {
                return comptime result: {
                    @setEvalBranchQuota(10_000);
                    for (codepoint_map) |entry| {
                        if (entry[1] == tag) break :result true;
                    }

                    break :result false;
                };
            },
        };
    }

    /// Returns true if this is a keypad key.
    pub fn keypad(self: Key) bool {
        return switch (self) {
            inline else => |tag| {
                const name = @tagName(tag);
                const result = comptime std.mem.startsWith(u8, name, "kp_");
                return result;
            },
        };
    }

    // Returns the codepoint representing this key, or null if the key is not
    // printable
    pub fn codepoint(self: Key) ?u21 {
        return switch (self) {
            inline else => |tag| {
                return comptime result: {
                    @setEvalBranchQuota(10_000);
                    for (codepoint_map) |entry| {
                        if (entry[1] == tag) break :result entry[0];
                    }

                    break :result null;
                };
            },
        };
    }

    test "fromASCII should not return keypad keys" {
        const testing = std.testing;
        try testing.expect(Key.fromASCII('0').? == .zero);
        try testing.expect(Key.fromASCII('*') == null);
    }

    test "keypad keys" {
        const testing = std.testing;
        try testing.expect(Key.kp_0.keypad());
        try testing.expect(!Key.one.keypad());
    }

    const codepoint_map: []const struct { u21, Key } = &.{
        .{ 'a', .a },
        .{ 'b', .b },
        .{ 'c', .c },
        .{ 'd', .d },
        .{ 'e', .e },
        .{ 'f', .f },
        .{ 'g', .g },
        .{ 'h', .h },
        .{ 'i', .i },
        .{ 'j', .j },
        .{ 'k', .k },
        .{ 'l', .l },
        .{ 'm', .m },
        .{ 'n', .n },
        .{ 'o', .o },
        .{ 'p', .p },
        .{ 'q', .q },
        .{ 'r', .r },
        .{ 's', .s },
        .{ 't', .t },
        .{ 'u', .u },
        .{ 'v', .v },
        .{ 'w', .w },
        .{ 'x', .x },
        .{ 'y', .y },
        .{ 'z', .z },
        .{ '0', .zero },
        .{ '1', .one },
        .{ '2', .two },
        .{ '3', .three },
        .{ '4', .four },
        .{ '5', .five },
        .{ '6', .six },
        .{ '7', .seven },
        .{ '8', .eight },
        .{ '9', .nine },
        .{ ';', .semicolon },
        .{ ' ', .space },
        .{ '\'', .apostrophe },
        .{ ',', .comma },
        .{ '`', .grave_accent },
        .{ '.', .period },
        .{ '/', .slash },
        .{ '-', .minus },
        .{ '=', .equal },
        .{ '[', .left_bracket },
        .{ ']', .right_bracket },
        .{ '\\', .backslash },

        // Keypad entries. We just assume keypad with the kp_ prefix
        // so that has some special meaning. These must also always be last.
        .{ '0', .kp_0 },
        .{ '1', .kp_1 },
        .{ '2', .kp_2 },
        .{ '3', .kp_3 },
        .{ '4', .kp_4 },
        .{ '5', .kp_5 },
        .{ '6', .kp_6 },
        .{ '7', .kp_7 },
        .{ '8', .kp_8 },
        .{ '9', .kp_9 },
        .{ '.', .kp_decimal },
        .{ '/', .kp_divide },
        .{ '*', .kp_multiply },
        .{ '-', .kp_subtract },
        .{ '+', .kp_add },
        .{ '=', .kp_equal },
    };
};
