//! xkeyboard-config — keyboard layouts, compiled from the X11 xkeyboard-config database
//! into native Zig. It turns a physical key (a USB HID usage, as the input module delivers)
//! plus a modifier state into a **keysym** and, when the key produces one, a **character**
//! (a Unicode scalar). This is the piece that lets a `KeyEvent.keycode` become a
//! `KeyEvent.character`, without shipping an X11 runtime.
//!
//! The layout tables in `generated/layouts.zig` are produced by
//! `tools/make-xkeyboard-config.py` (see ./README.md to regenerate). Those tables are
//! deliberately pure data — each key carries its up-to-four levels and an XKB *type*. The
//! type -> level selection semantics (which modifier picks which level) live here, so the
//! data and the policy are separable.
//!
//! Scope (documented in README.md): group 1 only, no dead-key/compose composition (a dead
//! key returns its keysym with no character), and a curated set of key types. Layouts:
//! us, gb, de, fr, es, dvorak.
//!
//! Upstream xkeyboard-config and keysymdef.h are MIT/X11 licensed; see vendor/COPYING and
//! vendor/PROVENANCE.md.

const std = @import("std");
const generated = @import("layouts");

pub const Level = generated.Level;
pub const KeyType = generated.KeyType;
pub const Key = generated.Key;
pub const Layout = generated.Layout;

/// The generated layouts, by name — as pointers, so they share identity with `all` and
/// `byName` (and match the `*const Layout` that `map` takes).
pub const us: *const Layout = &generated.us;
pub const gb: *const Layout = &generated.gb;
pub const de: *const Layout = &generated.de;
pub const fr: *const Layout = &generated.fr;
pub const es: *const Layout = &generated.es;
pub const dvorak: *const Layout = &generated.dvorak;

/// Every generated layout, for enumeration (e.g. a settings UI).
pub const all = generated.all;

/// The modifier state that selects a key's level. `level3` is AltGr (ISO Level3 Shift);
/// `control` is accepted for completeness but does not affect level selection here.
pub const Modifiers = struct {
    shift: bool = false,
    caps_lock: bool = false,
    level3: bool = false,
    control: bool = false,
};

/// The result of a lookup: the X11 `keysym`, and the `character` it produces (a Unicode
/// scalar) when it is a printable key — null for keys that produce none (Return, F1, a
/// bare dead key, an unmapped key).
pub const Mapping = struct {
    keysym: u32,
    character: ?u21,
};

/// Which level (0..3) a key of `kind` selects under `mods`. XKB's canonical semantics:
/// Shift picks the odd level, AltGr (level3) adds 2, and Caps acts like Shift for the
/// alphabetic types. See the XKB "key types" — this covers the ones the vendored layouts
/// use; anything else falls back to shift-or-not.
fn selectLevel(kind: KeyType, mods: Modifiers) usize {
    const shift_or_caps = mods.shift != mods.caps_lock; // XOR: Caps behaves like Shift
    const low: usize = if (mods.shift) 1 else 0;
    const high: usize = if (mods.level3) 2 else 0;
    return switch (kind) {
        .one_level => 0,
        .two_level, .keypad, .other => low,
        .alphabetic => if (shift_or_caps) 1 else 0,
        .four_level => low + high,
        .four_level_alphabetic => (if (shift_or_caps) @as(usize, 1) else 0) + high,
        // Caps affects only the base pair, not the AltGr pair.
        .four_level_semialphabetic => if (mods.level3) 2 + low else (if (shift_or_caps) @as(usize, 1) else 0),
    };
}

/// Map a physical key (`hid_usage`, a USB HID keyboard-page usage) under `mods` on
/// `layout` to its keysym and character. Falls back gracefully when the selected level is
/// undefined for the key: it drops the AltGr component, then the shift component, so a key
/// with only a base/shift pair still yields something sensible under AltGr.
pub fn map(layout: *const Layout, hid_usage: u8, mods: Modifiers) Mapping {
    const key = &layout.keys[hid_usage];
    var level = selectLevel(key.kind, mods);
    // Fall back to a defined level: full -> without AltGr -> base.
    if (key.levels[level].keysym == 0 and key.levels[level].unicode == 0) {
        const candidates = [_]usize{ level & 1, 0 };
        for (candidates) |candidate| {
            if (key.levels[candidate].keysym != 0 or key.levels[candidate].unicode != 0) {
                level = candidate;
                break;
            }
        }
    }
    const chosen = key.levels[level];
    return .{
        .keysym = chosen.keysym,
        .character = if (chosen.unicode != 0) @intCast(chosen.unicode) else null,
    };
}

/// Look up a layout by its name (`"us"`, `"gb"`, ...), or null if unknown.
pub fn byName(name: []const u8) ?*const Layout {
    for (all) |layout| {
        if (std.mem.eql(u8, layout.name, name)) return layout;
    }
    return null;
}

// --- tests (host-run via `zig build test`) ---------------------------------

const testing = std.testing;

// USB HID usages used in the tests (keyboard page 0x07).
const hid_a: u8 = 0x04;
const hid_1: u8 = 0x1e;
const hid_3: u8 = 0x20;

test "us: letters obey shift and caps" {
    try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{}).character);
    try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .shift = true }).character);
    try testing.expectEqual(@as(?u21, 'A'), map(us, hid_a, .{ .caps_lock = true }).character);
    // Shift + Caps cancels for an alphabetic key.
    try testing.expectEqual(@as(?u21, 'a'), map(us, hid_a, .{ .shift = true, .caps_lock = true }).character);
}

test "us: digits and their shifted symbols" {
    try testing.expectEqual(@as(?u21, '1'), map(us, hid_1, .{}).character);
    try testing.expectEqual(@as(?u21, '!'), map(us, hid_1, .{ .shift = true }).character);
    try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{}).character);
    try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character);
    // A digit is not alphabetic: Caps alone must not shift it.
    try testing.expectEqual(@as(?u21, '3'), map(us, hid_3, .{ .caps_lock = true }).character);
}

test "layouts differ: GB pound vs US hash on shift+3" {
    try testing.expectEqual(@as(?u21, '#'), map(us, hid_3, .{ .shift = true }).character);
    try testing.expectEqual(@as(?u21, '£'), map(gb, hid_3, .{ .shift = true }).character);
}

test "french azerty places q where us has a" {
    try testing.expectEqual(@as(?u21, 'q'), map(fr, hid_a, .{}).character);
    try testing.expectEqual(@as(?u21, 'Q'), map(fr, hid_a, .{ .shift = true }).character);
}

test "byName resolves and rejects" {
    try testing.expect(byName("us") == us);
    try testing.expect(byName("gb") == gb);
    try testing.expect(byName("nonsense") == null);
}

test "unmapped key yields no character" {
    // HID 0x00 is not a key; every level is empty.
    try testing.expectEqual(@as(?u21, null), map(us, 0x00, .{}).character);
}
