/*
 * Copyright © 2023 Pierre Le Marre <dev@wismill.eu>
 * SPDX-License-Identifier: MIT
 */

#include "config.h"

#include <assert.h>
#include <stdint.h>
#include <stdlib.h>

#include "xkbcommon/xkbcommon.h"
#include "xkbcommon/xkbcommon-names.h"
#include "evdev-scancodes.h"
#include "test.h"
#include "keymap.h"
#include "utils.h"

/* Standard real modifier masks */
enum real_mod_mask {
    ShiftMask   = (UINT32_C(1) << XKB_MOD_INDEX_SHIFT),
    LockMask    = (UINT32_C(1) << XKB_MOD_INDEX_CAPS),
    ControlMask = (UINT32_C(1) << XKB_MOD_INDEX_CTRL),
    Mod1Mask    = (UINT32_C(1) << XKB_MOD_INDEX_MOD1),
    Mod2Mask    = (UINT32_C(1) << XKB_MOD_INDEX_MOD2),
    Mod3Mask    = (UINT32_C(1) << XKB_MOD_INDEX_MOD3),
    Mod4Mask    = (UINT32_C(1) << XKB_MOD_INDEX_MOD4),
    Mod5Mask    = (UINT32_C(1) << XKB_MOD_INDEX_MOD5),
    NoModifier  = 0
};

static bool
test_real_mod(struct xkb_keymap *keymap, const char* name,
              xkb_mod_index_t idx, xkb_mod_mask_t mapping)
{
    return xkb_keymap_mod_get_index(keymap, name) == idx &&
           (keymap->mods.mods[idx].type == MOD_REAL) &&
           mapping == keymap->mods.mods[idx].mapping &&
           mapping == (UINT32_C(1) << idx) &&
           xkb_keymap_mod_get_mask(keymap, name) == mapping &&
           xkb_keymap_mod_get_mask2(keymap, idx) == mapping;
}

static bool
test_virtual_mod(struct xkb_keymap *keymap, const char* name,
                 xkb_mod_index_t idx, xkb_mod_mask_t mapping)
{
    return xkb_keymap_mod_get_index(keymap, name) == idx &&
           (keymap->mods.mods[idx].type == MOD_VIRT) &&
           mapping == keymap->mods.mods[idx].mapping &&
           xkb_keymap_mod_get_mask(keymap, name) == mapping &&
           xkb_keymap_mod_get_mask2(keymap, idx) == mapping;
}

/* Check that the provided modifier names work */
static void
test_modifiers_names(struct xkb_context *context)
{
    struct xkb_keymap *keymap;

    keymap = test_compile_rules(context, XKB_KEYMAP_FORMAT_TEXT_V1, "evdev",
                                "pc104", "us", NULL, NULL);
    assert(keymap);

    /* Real modifiers
     * The indices and masks are fixed and always valid */
    assert(test_real_mod(keymap, XKB_MOD_NAME_SHIFT, XKB_MOD_INDEX_SHIFT, ShiftMask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_CAPS, XKB_MOD_INDEX_CAPS, LockMask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_CTRL, XKB_MOD_INDEX_CTRL, ControlMask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_MOD1, XKB_MOD_INDEX_MOD1, Mod1Mask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_MOD2, XKB_MOD_INDEX_MOD2, Mod2Mask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_MOD3, XKB_MOD_INDEX_MOD3, Mod3Mask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_MOD4, XKB_MOD_INDEX_MOD4, Mod4Mask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_MOD5, XKB_MOD_INDEX_MOD5, Mod5Mask));

    /* Usual virtual mods mappings */
    assert(test_real_mod(keymap, XKB_MOD_NAME_ALT,  XKB_MOD_INDEX_MOD1, Mod1Mask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_NUM,  XKB_MOD_INDEX_MOD2, Mod2Mask));
    assert(test_real_mod(keymap, XKB_MOD_NAME_LOGO, XKB_MOD_INDEX_MOD4, Mod4Mask));

    /* Virtual modifiers
     * The indices depends on the keymap files */
    assert(test_virtual_mod(keymap, XKB_VMOD_NAME_ALT,    XKB_MOD_INDEX_MOD5 + 2,  Mod1Mask));
    assert(test_virtual_mod(keymap, XKB_VMOD_NAME_META,   XKB_MOD_INDEX_MOD5 + 11, Mod1Mask));
    assert(test_virtual_mod(keymap, XKB_VMOD_NAME_NUM,    XKB_MOD_INDEX_MOD5 + 1,  Mod2Mask));
    assert(test_virtual_mod(keymap, XKB_VMOD_NAME_SUPER,  XKB_MOD_INDEX_MOD5 + 12, Mod4Mask));
    assert(test_virtual_mod(keymap, XKB_VMOD_NAME_HYPER,  XKB_MOD_INDEX_MOD5 + 13, Mod4Mask));
    assert(test_virtual_mod(keymap, XKB_VMOD_NAME_LEVEL3, XKB_MOD_INDEX_MOD5 + 3,  Mod5Mask));
    assert(test_virtual_mod(keymap, XKB_VMOD_NAME_SCROLL, XKB_MOD_INDEX_MOD5 + 8,  0       ));
    /* TODO: current xkeyboard-config maps LevelFive to Nod3 by default */
    assert(test_virtual_mod(keymap, XKB_VMOD_NAME_LEVEL5, XKB_MOD_INDEX_MOD5 + 9,  0));
    /* Legacy stuff, removed from xkeyboard-config */
    assert(keymap->mods.num_mods == 21);
    assert(test_virtual_mod(keymap, "LAlt",     XKB_MOD_INDEX_MOD5 + 4,  0       ));
    assert(test_virtual_mod(keymap, "RAlt",     XKB_MOD_INDEX_MOD5 + 5,  0       ));
    assert(test_virtual_mod(keymap, "LControl", XKB_MOD_INDEX_MOD5 + 7,  0       ));
    assert(test_virtual_mod(keymap, "RControl", XKB_MOD_INDEX_MOD5 + 6,  0       ));
    assert(test_virtual_mod(keymap, "AltGr",    XKB_MOD_INDEX_MOD5 + 10, Mod5Mask));

    xkb_keymap_unref(keymap);
}

static void
test_modmap_none(struct xkb_context *context)
{
    struct xkb_keymap *keymap;
    const struct xkb_key *key;
    xkb_keycode_t keycode;

    keymap = test_compile_file(
        context, XKB_KEYMAP_FORMAT_TEXT_V1, "keymaps/modmap-none.xkb"
    );
    assert(keymap);

    keycode = xkb_keymap_key_by_name(keymap, "LVL3");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == NoModifier);

    keycode = xkb_keymap_key_by_name(keymap, "LFSH");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == NoModifier);

    keycode = xkb_keymap_key_by_name(keymap, "RTSH");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == NoModifier);

    keycode = xkb_keymap_key_by_name(keymap, "LWIN");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod4Mask);

    keycode = xkb_keymap_key_by_name(keymap, "RWIN");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod4Mask);

    keycode = xkb_keymap_key_by_name(keymap, "LCTL");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == ControlMask);

    keycode = xkb_keymap_key_by_name(keymap, "RCTL");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == ControlMask);

    keycode = xkb_keymap_key_by_name(keymap, "LALT");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod1Mask);

    keycode = xkb_keymap_key_by_name(keymap, "RALT");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == (Mod2Mask | Mod5Mask));

    keycode = xkb_keymap_key_by_name(keymap, "CAPS");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == LockMask);

    keycode = xkb_keymap_key_by_name(keymap, "AD01");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod1Mask);

    keycode = xkb_keymap_key_by_name(keymap, "AD02");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == NoModifier);

    keycode = xkb_keymap_key_by_name(keymap, "AD03");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == NoModifier);

    keycode = xkb_keymap_key_by_name(keymap, "AD04");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod1Mask);

    keycode = xkb_keymap_key_by_name(keymap, "AD05");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod2Mask);

    keycode = xkb_keymap_key_by_name(keymap, "AD06");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod3Mask);

    keycode = xkb_keymap_key_by_name(keymap, "AD07");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod1Mask);

    keycode = xkb_keymap_key_by_name(keymap, "AD08");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod2Mask);

    keycode = xkb_keymap_key_by_name(keymap, "AD09");
    assert(keycode != XKB_KEYCODE_INVALID);
    key = XkbKey(keymap, keycode);
    assert(key->modmap == Mod3Mask);

    xkb_keymap_unref(keymap);
}

static void
test_explicit_virtual_modifiers(struct xkb_context *context)
{
    const struct {
        const char* keymap;
        struct mod_props {
            enum mod_type type;
            xkb_mod_mask_t mapping;
            xkb_mod_mask_t mapping_effective;
        } m1;
        struct mod_props m2;
    } tests[] = {
        /* Test virtual modifiers with canonical mappings */
        {
            .keymap =
                "xkb_keymap {\n"
                "  xkb_compat {\n"
                /* Vmods map to themselves */
                "    virtual_modifiers M1 = 0x100, M2 = 0x200;\n"
                "  };\n"
                "};",
            .m1 = {
                .type = MOD_VIRT,
                .mapping = 0x100,
                .mapping_effective = 0x100
            },
            .m2 = {
                .type = MOD_VIRT,
                .mapping = 0x200,
                .mapping_effective = 0x200
            }
        },
        /* Test virtual modifiers overlapping: identical */
        {
            .keymap =
                "xkb_keymap {\n"
                "  xkb_compat {\n"
                "    virtual_modifiers M1 = 0x100, M2 = 0x100;\n"
                "  };\n"
                "};",
            .m1 = {
                .type = MOD_VIRT,
                .mapping = 0x100,
                .mapping_effective = 0x100
            },
            .m2 = {
                .type = MOD_VIRT,
                .mapping = 0x100,
                .mapping_effective = 0x100
            }
        },
        /* Test virtual modifiers overlapping: non identical */
        {
            .keymap =
                "xkb_keymap {\n"
                "  xkb_compat {\n"
                "    virtual_modifiers M1 = 0x100, M2 = 0x300;\n"
                "  };\n"
                "};",
            .m1 = {
                .type = MOD_VIRT,
                .mapping = 0x100,
                .mapping_effective = 0x100
            },
            .m2 = {
                .type = MOD_VIRT,
                .mapping = 0x300,
                .mapping_effective = 0x300
            }
        },
        /* Test virtual modifiers with swapped mappings */
        {
            .keymap =
                "xkb_keymap {\n"
                "  xkb_compat {\n"
                /* The mapping of each modifier is the mask of the other */
                "    virtual_modifiers M1 = 0x200, M2 = 0x100;\n"
                "  };\n"
                "};",
            .m1 = {
                .type = MOD_VIRT,
                .mapping = 0x200,
                .mapping_effective = 0x100 /* different from mapping! */
            },
            .m2 = {
                .type = MOD_VIRT,
                .mapping = 0x100,
                .mapping_effective = 0x200 /* different from mapping! */
            }
        },
        /* Test virtual modifiers mapping to undefined modifiers */
        {
            .keymap =
                "xkb_keymap {\n"
                "  xkb_compat {\n"
                "    virtual_modifiers M1 = 0x400, M2 = 0x800;\n"
                "  };\n"
                "};",
            .m1 = {
                .type = MOD_VIRT,
                .mapping = 0x400,
                .mapping_effective = 0 /* no mod entry */
            },
            .m2 = {
                .type = MOD_VIRT,
                .mapping = 0x800,
                .mapping_effective = 0 /* no mod entry */
            }
        },
    };

    for (unsigned int k = 0; k < ARRAY_SIZE(tests); k++) {
        fprintf(stderr, "------\n*** %s: #%u ***\n", __func__, k);
        struct xkb_keymap *keymap = test_compile_buffer(
            context, XKB_KEYMAP_FORMAT_TEXT_V1,
            tests[k].keymap, strlen(tests[k].keymap)
        );
        assert(keymap);

        const xkb_mod_index_t m1_idx = xkb_keymap_mod_get_index(keymap, "M1");
        const xkb_mod_index_t m2_idx = xkb_keymap_mod_get_index(keymap, "M2");
        assert(m1_idx == 8);
        assert(m2_idx == 9);
        assert(keymap->mods.mods[m1_idx].type == tests[k].m1.type);
        assert(keymap->mods.mods[m2_idx].type == tests[k].m2.type);

        const xkb_mod_mask_t m1 = UINT32_C(1) << m1_idx;
        const xkb_mod_mask_t m2 = UINT32_C(1) << m2_idx;
        const xkb_mod_mask_t m1_mapping = mod_mask_get_effective(keymap, m1);
        const xkb_mod_mask_t m2_mapping = mod_mask_get_effective(keymap, m2);
        assert(m1_mapping == tests[k].m1.mapping);
        assert(m2_mapping == tests[k].m2.mapping);
        /* mod_mask_get_effective is not idempotent */
        assert(mod_mask_get_effective(keymap, m1_mapping) ==
               tests[k].m1.mapping_effective);
        assert(mod_mask_get_effective(keymap, m2_mapping) ==
               tests[k].m2.mapping_effective);

        struct xkb_state *state = xkb_state_new(keymap);
        assert(state);

        /* Not in the canonical modifier mask nor denotes a *known* virtual
         * modifiers, so it will be discarded. */
        const xkb_mod_mask_t noise = UINT32_C(0x8000);
        assert((keymap->canonical_state_mask & noise) == 0);

        /* Update the state, then check round-trip and mods state */
        const xkb_mod_mask_t set_masks[] = {m1_mapping, m2_mapping};
        for (unsigned int m = 0; m < ARRAY_SIZE(set_masks); m++) {
            const xkb_mod_mask_t expected = set_masks[m];
            xkb_state_update_mask(state, expected | noise, 0, noise, 0, 0, 0);
            const xkb_mod_mask_t got =
                xkb_state_serialize_mods(state, XKB_STATE_MODS_EFFECTIVE);
            assert_printf(got == expected, /* round-trip */
                          "expected %#"PRIx32", got %#"PRIx32"\n",
                          expected, got);
            assert(
                xkb_state_mod_index_is_active(state, m1_idx,
                                              XKB_STATE_MODS_EFFECTIVE) ==
                ((expected & m1_mapping) == m1_mapping)
            );
            assert(
                xkb_state_mod_index_is_active(state, m2_idx,
                                              XKB_STATE_MODS_EFFECTIVE) ==
                ((expected & m2_mapping) == m2_mapping)
            );
        }

        xkb_state_unref(state);
        xkb_keymap_unref(keymap);
    }
}

/*
 * Test the hack documented in the FAQ to get virtual modifiers mapping using
 * `xkb_state_update_mask`/`xkb_state_serialize_mods`.
 *
 * This should work without problem for keymap using only real mods to map
 * virtual modifiers.
 *
 * [NOTE] If the test requires an update, do not forget to update the FAQ as well!
 */
static void
test_virtual_modifiers_mapping_hack(struct xkb_context *context)
{
    struct xkb_keymap *keymap = test_compile_rules(context,
                                                   XKB_KEYMAP_FORMAT_TEXT_V1,
                                                   "evdev", "pc104",
                                                   "us", NULL, NULL);
    assert(keymap);

    struct xkb_state* state = xkb_state_new(keymap);
    assert(state);

    static const struct {
        const char* name;
        xkb_mod_index_t index;
        xkb_mod_mask_t mapping;
    } mods[] = {
        /* Real modifiers */
        {
            .name = XKB_MOD_NAME_SHIFT,
            .index = XKB_MOD_INDEX_SHIFT,
            .mapping = UINT32_C(1) << XKB_MOD_INDEX_SHIFT
        },
        {
            .name = XKB_MOD_NAME_CAPS,
            .index = XKB_MOD_INDEX_CAPS,
            .mapping = UINT32_C(1) << XKB_MOD_INDEX_CAPS
        },
        {
            .name = XKB_MOD_NAME_CTRL,
            .index = XKB_MOD_INDEX_CTRL,
            .mapping = UINT32_C(1) << XKB_MOD_INDEX_CTRL
        },
        {
            .name = XKB_MOD_NAME_MOD1,
            .index = XKB_MOD_INDEX_MOD1,
            .mapping = UINT32_C(1) << XKB_MOD_INDEX_MOD1
        },
        {
            .name = XKB_MOD_NAME_MOD2,
            .index = XKB_MOD_INDEX_MOD2,
            .mapping = UINT32_C(1) << XKB_MOD_INDEX_MOD2
        },
        {
            .name = XKB_MOD_NAME_MOD3,
            .index = XKB_MOD_INDEX_MOD3,
            .mapping = UINT32_C(1) << XKB_MOD_INDEX_MOD3
        },
        {
            .name = XKB_MOD_NAME_MOD4,
            .index = XKB_MOD_INDEX_MOD4,
            .mapping = UINT32_C(1) << XKB_MOD_INDEX_MOD4
        },
        {
            .name = XKB_MOD_NAME_MOD5,
            .index = XKB_MOD_INDEX_MOD5,
            .mapping = UINT32_C(1) << XKB_MOD_INDEX_MOD5
        },
        /* Virtual modifiers
         * The indices depends on the keymap files */
        {
            .name = XKB_VMOD_NAME_ALT,
            .index = XKB_MOD_INDEX_MOD5 + 2,
            .mapping = Mod1Mask
        },
        {
            .name = XKB_VMOD_NAME_META,
            .index = XKB_MOD_INDEX_MOD5 + 11,
            .mapping = Mod1Mask
        },
        {
            .name = XKB_VMOD_NAME_NUM,
            .index = XKB_MOD_INDEX_MOD5 + 1,
            .mapping = Mod2Mask
        },
        {
            .name = XKB_VMOD_NAME_SUPER,
            .index = XKB_MOD_INDEX_MOD5 + 12,
            .mapping = Mod4Mask
        },
        {
            .name = XKB_VMOD_NAME_HYPER,
            .index = XKB_MOD_INDEX_MOD5 + 13,
            .mapping = Mod4Mask
        },
        {
            .name = XKB_VMOD_NAME_LEVEL3,
            .index = XKB_MOD_INDEX_MOD5 + 3,
            .mapping = Mod5Mask
        },
        {
            .name = XKB_VMOD_NAME_SCROLL,
            .index = XKB_MOD_INDEX_MOD5 + 8,
            .mapping = 0
        },
        {
            .name = XKB_VMOD_NAME_LEVEL5,
            .index = XKB_MOD_INDEX_MOD5 + 9,
            .mapping = 0
        },
        /* Legacy stuff, removed from xkeyboard-config */
        {
            .name = "LAlt",
            .index = XKB_MOD_INDEX_MOD5 + 4,
            .mapping = 0
        },
        {
            .name = "RAlt",
            .index = XKB_MOD_INDEX_MOD5 + 5,
            .mapping = 0
        },
        {
            .name = "LControl",
            .index = XKB_MOD_INDEX_MOD5 + 7,
            .mapping = 0
        },
        {
            .name = "RControl",
            .index = XKB_MOD_INDEX_MOD5 + 6,
            .mapping = 0
        },
        {
            .name = "AltGr",
            .index = XKB_MOD_INDEX_MOD5 + 10,
            .mapping = Mod5Mask
        },
    };

    for (unsigned int k = 0; k < ARRAY_SIZE(mods); k++) {
        const xkb_mod_mask_t index =
            xkb_keymap_mod_get_index(keymap, mods[k].name);
        assert_printf(index == mods[k].index,
                      "%s: expected %"PRIu32", got: %"PRIu32"\n",
                      mods[k].name, mods[k].index, index);
        const xkb_mod_mask_t mask = UINT32_C(1) << index;
        xkb_state_update_mask(state, mask, 0, 0, 0, 0, 0);
        const xkb_mod_mask_t mapping =
            xkb_state_serialize_mods(state, XKB_STATE_MODS_EFFECTIVE);
        assert_printf(mapping == mods[k].mapping,
                      "%s: expected %#"PRIx32", got: %#"PRIx32"\n",
                      mods[k].name, mods[k].mapping, mapping);
        assert(mapping == xkb_keymap_mod_get_mask(keymap, mods[k].name));
        assert(mapping == xkb_keymap_mod_get_mask2(keymap, mods[k].index));
    }

    xkb_state_unref(state);
    xkb_keymap_unref(keymap);
}

static void
test_pure_virtual_modifiers(struct xkb_context *context)
{
    struct xkb_keymap *keymap;

    /* Test definition of >20 pure virtual modifiers.
     * We overcome the X11 limit of 16 virtual modifiers. */
    struct {
        const char* path;
        enum xkb_keymap_format formats[2];
    } keymaps[] = {
        {
            .path = "keymaps/pure-virtual-mods-explicit.xkb",
            .formats = { XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_FORMAT_TEXT_V2 }
        },
        {
            .path = "keymaps/pure-virtual-mods-implicit.xkb",
            /* Auto canonical vmod available since keymap format v2 */
            .formats = { XKB_KEYMAP_FORMAT_TEXT_V2 }
        }
    };
    for (unsigned int k = 0; k < ARRAY_SIZE(keymaps); k++) {
    for (unsigned int f = 0; f < ARRAY_SIZE(keymaps[k].formats); f++) {
        if (!keymaps[k].formats[f])
            break;
        fprintf(stderr, "------\n*** %s: #%u %s (%d) ***\n",
                __func__, k, keymaps[k].path, keymaps[k].formats[f]);
        keymap = test_compile_file(context, keymaps[k].formats[f],
                                   keymaps[k].path);
        assert(keymap);

        assert(test_key_seq(keymap,
                            KEY_W,          BOTH,  XKB_KEY_w,        NEXT,
                            KEY_A,          DOWN,  XKB_KEY_a,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_a,        NEXT,
                            KEY_A,          UP,    XKB_KEY_a,        NEXT,
                            KEY_B,          DOWN,  XKB_KEY_b,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_b,        NEXT,
                            KEY_B,          UP,    XKB_KEY_b,        NEXT,
                            KEY_C,          DOWN,  XKB_KEY_c,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_c,        NEXT,
                            KEY_C,          UP,    XKB_KEY_c,        NEXT,
                            KEY_D,          DOWN,  XKB_KEY_d,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_d,        NEXT,
                            KEY_D,          UP,    XKB_KEY_d,        NEXT,
                            KEY_E,          DOWN,  XKB_KEY_e,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_e,        NEXT,
                            KEY_E,          UP,    XKB_KEY_e,        NEXT,
                            KEY_F,          DOWN,  XKB_KEY_f,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_f,        NEXT,
                            KEY_F,          UP,    XKB_KEY_f,        NEXT,
                            KEY_G,          DOWN,  XKB_KEY_g,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_g,        NEXT,
                            KEY_G,          UP,    XKB_KEY_g,        NEXT,
                            KEY_H,          DOWN,  XKB_KEY_h,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_h,        NEXT,
                            KEY_H,          UP,    XKB_KEY_h,        NEXT,
                            KEY_I,          DOWN,  XKB_KEY_i,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_i,        NEXT,
                            KEY_I,          UP,    XKB_KEY_i,        NEXT,
                            KEY_J,          DOWN,  XKB_KEY_j,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_j,        NEXT,
                            KEY_J,          UP,    XKB_KEY_j,        NEXT,
                            KEY_K,          DOWN,  XKB_KEY_k,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_k,        NEXT,
                            KEY_K,          UP,    XKB_KEY_k,        NEXT,
                            KEY_L,          DOWN,  XKB_KEY_l,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_l,        NEXT,
                            KEY_L,          UP,    XKB_KEY_l,        NEXT,
                            KEY_M,          DOWN,  XKB_KEY_m,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_m,        NEXT,
                            KEY_M,          UP,    XKB_KEY_m,        NEXT,
                            KEY_N,          DOWN,  XKB_KEY_n,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_n,        NEXT,
                            KEY_N,          UP,    XKB_KEY_n,        NEXT,
                            KEY_O,          DOWN,  XKB_KEY_o,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_o,        NEXT,
                            KEY_O,          UP,    XKB_KEY_o,        NEXT,
                            KEY_P,          DOWN,  XKB_KEY_p,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_p,        NEXT,
                            KEY_P,          UP,    XKB_KEY_p,        NEXT,
                            KEY_Q,          DOWN,  XKB_KEY_q,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_q,        NEXT,
                            KEY_Q,          UP,    XKB_KEY_q,        NEXT,
                            KEY_R,          DOWN,  XKB_KEY_r,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_r,        NEXT,
                            KEY_R,          UP,    XKB_KEY_r,        NEXT,
                            KEY_S,          DOWN,  XKB_KEY_s,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_s,        NEXT,
                            KEY_S,          UP,    XKB_KEY_s,        NEXT,
                            KEY_T,          DOWN,  XKB_KEY_t,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_t,        NEXT,
                            KEY_T,          UP,    XKB_KEY_t,        NEXT,
                            KEY_U,          DOWN,  XKB_KEY_u,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_u,        NEXT,
                            KEY_U,          UP,    XKB_KEY_u,        NEXT,
                            KEY_V,          DOWN,  XKB_KEY_v,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_v,        NEXT,
                            KEY_LEFTSHIFT,  DOWN,  XKB_KEY_Shift_L,  NEXT,
                            KEY_W,          BOTH,  XKB_KEY_V,        NEXT,
                            KEY_LEFTSHIFT,  UP,    XKB_KEY_Shift_L,  NEXT,
                            KEY_V,          UP,    XKB_KEY_v,        NEXT,
                            KEY_A,          DOWN,  XKB_KEY_a,        NEXT,
                            KEY_S,          DOWN,  XKB_KEY_s,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_1,        NEXT,
                            KEY_RIGHTALT,   DOWN,  XKB_KEY_ISO_Level3_Shift, NEXT,
                            KEY_W,          BOTH,  XKB_KEY_4,        NEXT,
                            KEY_S,          UP,    XKB_KEY_s,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_3,        NEXT,
                            KEY_RIGHTALT,   UP,    XKB_KEY_ISO_Level3_Shift, NEXT,
                            KEY_Q,          DOWN,  XKB_KEY_q,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_2,        NEXT,
                            KEY_Q,          UP,    XKB_KEY_q,        NEXT,
                            KEY_B,          DOWN,  XKB_KEY_b,        NEXT,
                            KEY_C,          DOWN,  XKB_KEY_c,        NEXT,
                            KEY_W,          BOTH,  XKB_KEY_5,        NEXT,
                            KEY_C,          UP,    XKB_KEY_c,        NEXT,
                            KEY_B,          UP,    XKB_KEY_b,        NEXT,
                            KEY_A,          UP,    XKB_KEY_a,        NEXT,
                            KEY_Y,          BOTH,  XKB_KEY_y,        FINISH));
        xkb_keymap_unref(keymap);
    }}

    /* Test invalid interpret using a virtual modifier */
    const char keymap_str[] =
        "xkb_keymap {"
        "  xkb_keycodes { include \"evdev\" };"
        "  xkb_types { include \"complete\" };"
        "  xkb_compat { include \"complete+basic(invalid-pure-virtual-modifiers)\" };"
        "  xkb_symbols { include \"pc(pc105-pure-virtual-modifiers)\" };"
        "};";
    keymap = test_compile_string(context, XKB_KEYMAP_FORMAT_TEXT_V1, keymap_str);
    assert(!keymap);
}

/* Test FAQ snippet */
//! [xkb_keymap_mod_get_codes]
#include <xkbcommon/xkbcommon.h>
/**
 * Get the keycodes associated with a modifier by index.
 *
 * @param[in] keymap The keymap
 * @param[in] idx The index of the modifier
 * @param[out] codes_out A buffer in which the codes should be stored
 * @param[out] codes_size The size of the codes_out buffer
 *
 * @returns the number of key codes in the keysyms_out array, or a negative
 * in case of error.
 */
static ssize_t
xkb_keymap_mod_get_codes(struct xkb_keymap *keymap, xkb_mod_index_t mod,
                         xkb_keycode_t *codes_out, size_t codes_size)
{
    if (mod >= xkb_keymap_num_mods(keymap))
        return -1;

    ssize_t idx = 0;
    for (xkb_keycode_t k = xkb_keymap_min_keycode(keymap);
         k <= xkb_keymap_max_keycode(keymap) && idx >= 0; k++) {
        /*
         * We only test the first level of the first group.
         *
         * Since there is currently no way to reset the state, use a new one to
         * avoid side effects (latches, etc.)
         */
        struct xkb_state * const state = xkb_state_new(keymap);
        assert(state);

        static const enum xkb_key_direction directions[] = {
            XKB_KEY_DOWN,
            XKB_KEY_UP
        };

        for (size_t d = 0; d < ARRAY_SIZE(directions); d++) {
            if (xkb_state_update_key(state, k, directions[d]) &&
                xkb_state_mod_index_is_active(state, mod,
                                              XKB_STATE_MODS_EFFECTIVE)) {
                if ((size_t) idx < codes_size) {
                    assert(idx < (ssize_t) (SIZE_MAX / 2));
                    codes_out[idx++] = k;
                } else {
                    idx = -2;
                }
                break;
            }
        }

        xkb_state_unref(state);
    }
    return idx;
}
//! [xkb_keymap_mod_get_codes]

static void
test_get_modifier_keycodes(struct xkb_context *context)
{
    struct xkb_keymap *keymap = test_compile_rules(context,
                                                   XKB_KEYMAP_FORMAT_TEXT_V1,
                                                   "evdev", "pc104",
                                                   "cz", NULL, NULL);
    assert(keymap);

#define MAX_KEYCODES_COUNT 10
#define KEY_LVL3 (92 - EVDEV_OFFSET)
    const struct {
        const char *mod;
        xkb_keycode_t keycodes[MAX_KEYCODES_COUNT];
    } mods[] = {
        { .mod = XKB_MOD_NAME_SHIFT, .keycodes = { KEY_LEFTSHIFT, KEY_RIGHTSHIFT, 0 } },
        { .mod = XKB_MOD_NAME_CAPS, .keycodes = { KEY_CAPSLOCK, 0 } },
        { .mod = XKB_MOD_NAME_CTRL, .keycodes = { KEY_LEFTCTRL, KEY_RIGHTCTRL, 0 } },
        { .mod = XKB_MOD_NAME_MOD1, .keycodes = { KEY_LEFTALT, 0 } },
        { .mod = XKB_MOD_NAME_MOD2, .keycodes = { KEY_NUMLOCK, 0 } },
        { .mod = XKB_MOD_NAME_MOD3, .keycodes = { 0 } },
        { .mod = XKB_MOD_NAME_MOD4, .keycodes = { KEY_LEFTMETA, KEY_RIGHTMETA, 0 } },
        { .mod = XKB_MOD_NAME_MOD5, .keycodes = { KEY_LVL3, KEY_RIGHTALT, 0 } },
        { .mod = XKB_VMOD_NAME_ALT, .keycodes = { KEY_LEFTALT, 0 } },
        { .mod = XKB_VMOD_NAME_META, .keycodes = { KEY_LEFTALT, 0 } },
        { .mod = XKB_VMOD_NAME_SUPER, .keycodes = { KEY_LEFTMETA, KEY_RIGHTMETA, 0 } },
        { .mod = XKB_VMOD_NAME_HYPER, .keycodes = { KEY_LEFTMETA, KEY_RIGHTMETA, 0 } },
        { .mod = XKB_VMOD_NAME_NUM, .keycodes = { KEY_NUMLOCK, 0 } },
        { .mod = XKB_VMOD_NAME_LEVEL3, .keycodes = { KEY_LVL3, KEY_RIGHTALT, 0 } },
        { .mod = XKB_VMOD_NAME_LEVEL5, .keycodes = { 0 } },
    };

    xkb_keycode_t got[MAX_KEYCODES_COUNT] = {0};
    for (size_t m = 0; m < ARRAY_SIZE(mods); m++) {
        const xkb_mod_index_t mod = xkb_keymap_mod_get_index(keymap, mods[m].mod);
        assert(mod != XKB_MOD_INVALID);
        const ssize_t count =
            xkb_keymap_mod_get_codes(keymap, mod, got, ARRAY_SIZE(got));
        assert(count >= 0);
        const xkb_keycode_t * const expected = mods[m].keycodes;
        for (size_t k = 0; k < ARRAY_SIZE(got); k++) {
            if (expected[k] == 0) {
                assert_printf(k == (size_t) count,
                              "Mod %s: expected %zu, got %zd\n",
                              mods[m].mod, k, count);
                break;
            }
            assert_printf(k < (size_t) count,
                          "Mod %s: Missing keycode 0x%"PRIu32"\n",
                          mods[m].mod, expected[k]);
            assert_printf(got[k] == expected[k] + EVDEV_OFFSET,
                          "Mod %s: Expected keycode %"PRIu32" but got: %"PRIu32"\n",
                          mods[m].mod, expected[k] + EVDEV_OFFSET, got[k]);
        }
    }

    xkb_keymap_unref(keymap);
}
#undef MAX_KEYCODES_COUNT

int
main(void)
{
    test_init();

    struct xkb_context *context = test_get_context(CONTEXT_NO_FLAG);
    assert(context);

    test_modmap_none(context);
    test_modifiers_names(context);
    test_explicit_virtual_modifiers(context);
    test_virtual_modifiers_mapping_hack(context);
    test_pure_virtual_modifiers(context);
    test_get_modifier_keycodes(context);

    xkb_context_unref(context);

    return 0;
}
