#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#include "nuklear.h"
#include "menu.h"
#include "widget.h"
#include "../interface/katconfig.h"
#include "../interface/input.h"
#include "../interface/sdl_if.h"
#include "../core/pokey.h"
#include "modal.h"
#include "guitop.h"
#include "theme.h"

extern struct theme g_theme;

/* Static masks for tracking changes */
static unsigned long long input_ui_mask = 0;
static unsigned long long input_player_mask[MAX_CONTROLLER] = {0};
static unsigned long long input_keyboard_mask = 0;
static e_dev_type input_joy_slot_types[MAX_PC_DEVICE];

static int input_require_rom(void)
{
    if (!config_ensure_rom_table()) {
        guitop_show_toast_msg("No ROM loaded.");
        return 0;
    }

    return 1;
}

/* Context for input action menu */
static struct {
    t_input_common *input;          /* Current t_input_common being acted on */
    t_input_common *list_head;      /* Head of the linked list this belongs to */
    t_player_input *player;         /* Player owning this input, if any */
    int prev_menu;                  /* Menu to return to */
    int input_empty_row;            /* Placeholder row with no visible mapping */
    unsigned long long *p_mask;     /* Pointer to mask for tracking changes */
    unsigned long long mask_value;  /* Mask value for this input */
    char label[64];                 /* Label from the widget */
} input_action_ctx = {0};

/* State for input capture modal */
typedef enum {
    CAPTURE_IDLE,
    CAPTURE_WAITING_RELEASE,   /* Waiting for navigation input to be released */
    CAPTURE_WAITING,           /* Waiting for any input */
    CAPTURE_WAITING_KEY_UP     /* Key down received, waiting for key up to detect modifiers */
} e_capture_state;

static struct {
    e_capture_state state;
    int is_add;                    /* 1 = adding new input, 0 = changing existing */
    t_input_common *target;        /* Input to modify (for change) or list to append to (for add) */
    t_input_common *list_head;     /* Head of the linked list */
    t_player_input *player;        /* Player owning this input, if any */
    unsigned long long *p_mask;    /* Mask pointer for tracking changes */
    unsigned long long mask_value; /* Mask value */
    char label[64];                /* Label for display */
    int pending_releases;          /* Count of inputs still held from navigation */
    int had_button_down;           /* A button DOWN was seen in CAPTURE_WAITING (guards against spurious UP) */
    /* Captured input values */
    e_dev_type captured_device;
    int captured_device_num;
    e_part_type captured_part_type;
    int captured_part_num;
    e_direction captured_direction;
    int captured_key_mod;          /* Modifier keys held during capture */
    int captured_key_sym;          /* The key that was pressed */
} input_capture_ctx = {0};

/* Context for joystick selection sub-menu */
static struct {
    int player_num;        /* Which player we're selecting for */
    int selected_index;    /* Currently selected valid joy slot (0-based) */
    int prev_menu;         /* Return menu (KAT_MENU_OPTIONS_INPUT_Px) */
} joy_select_ctx = {0};

/* get_string callback: display mouse optimize target */
static char *input_get_mouse_optimize_string(struct option_widget *wgt, char *label)
{
    switch (*(int *)(wgt->data)) {
        case MOUSE_OPTIMIZE_CURSOR:  strcpy(label, "Cursor");  break;
        default:                     strcpy(label, "Paddles"); break;
    }
    return label;
}

/* get_string callback: display current joystick binding for a player */
static char *input_joy_name_string(struct option_widget *wgt, char *buf)
{
    int player_num = (int)(intptr_t)wgt->data;
    t_config *cfg = config_get_global_cfg();
    t_player_input *player = &cfg->input.game.players[player_num];
    pc_get_player_joy_name(player->joy_guid, buf, 47);
    return buf;
}

/* run_task callback: set up joy_select_ctx when entering the sub-menu */
static void input_joy_select_setup(struct option_widget *wgt, void *data)
{
    static const int player_menus[] = {
        KAT_MENU_OPTIONS_INPUT_P1, KAT_MENU_OPTIONS_INPUT_P2,
        KAT_MENU_OPTIONS_INPUT_P3, KAT_MENU_OPTIONS_INPUT_P4
    };
    int player_num = (int)(intptr_t)data;
    t_config *cfg = config_get_global_cfg();
    t_player_input *player = &cfg->input.game.players[player_num];

    joy_select_ctx.player_num = player_num;
    joy_select_ctx.prev_menu  = player_menus[player_num];
    joy_select_ctx.selected_index = 0;

    /* Pre-select the joystick that matches the player's stored GUID */
    if (player->joy_guid[0]) {
        int n = pc_get_valid_joy_count();
        for (int j = 0; j < n; j++) {
            char guid_str[48];
            pc_get_joy_guid_string(pc_get_valid_joy_id(j), guid_str, sizeof(guid_str));
            if (strcmp(guid_str, player->joy_guid) == 0) {
                joy_select_ctx.selected_index = j;
                break;
            }
        }
    }
    (void)wgt;
}

/* Save helpers for joy select sub-menu */
static void joy_select_save(t_toml_node *tbl, const char *scope)
{
    int player_num = joy_select_ctx.player_num;
    t_config *cfg = config_get_global_cfg();
    t_player_input *player = &cfg->input.game.players[player_num];
    unsigned int joy_id = pc_get_valid_joy_id(joy_select_ctx.selected_index);

    if (joy_id) {
        pc_get_joy_guid_string(joy_id, player->joy_guid, sizeof(player->joy_guid));
        pc_get_joy_serial(joy_id, player->joy_serial, sizeof(player->joy_serial));
    } else {
        player->joy_guid[0] = '\0';
        player->joy_serial[0] = '\0';
    }
    input_player_mask[player_num] |= KCFG_INP_JOY_GUID | KCFG_INP_JOY_SERIAL;
    config_save_game_input(cfg->general.machine, tbl, scope, input_player_mask, 0);
    input_player_mask[player_num] = 0;
}

static void joy_select_save_all(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    joy_select_save(config_get_global_table(), "All");
    guitop_show_saved_toast("Settings saved.");
}

static void joy_select_save_rom(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    if (!input_require_rom())
        return;

    joy_select_save(config_get_rom_table(), "this");
    config_refresh_rom_source_masks();
    guitop_show_toast_msg("Saved for this ROM.");
}

/* get_string callback for the SELECT widget: display slot index */
static char *joy_select_index_string(struct option_widget *wgt, char *buf)
{
    int idx = *((int *)wgt->data);
    unsigned int joy_id = pc_get_valid_joy_id(idx);
    if (joy_id) {
        const char *name = SDL_GetJoystickNameForID(joy_id);
        snprintf(buf, 47, "%s", name ? name : "Unknown");
    } else {
        snprintf(buf, 47, "None");
    }
    return buf;
}

/* Save functions for player input */
static void input_player_save_all(struct option_widget *wgt, void *data)
{
    t_config *cfg = config_get_global_cfg();
    int player_num = (int)(intptr_t)data;

    (void)wgt;
    config_save_game_input(cfg->general.machine, config_get_global_table(),
        "All", input_player_mask, 0);
    input_player_mask[player_num] = 0;
    guitop_show_saved_toast("Settings saved.");
}

static void input_player_save_rom(struct option_widget *wgt, void *data)
{
    t_config *cfg = config_get_global_cfg();
    int player_num = (int)(intptr_t)data;

    (void)wgt;
    if (!input_require_rom())
        return;

    config_save_game_input(cfg->general.machine, config_get_rom_table(),
        "this", input_player_mask, 0);
    input_player_mask[player_num] = 0;
    config_refresh_rom_source_masks();
    guitop_show_toast_msg("Saved for this ROM.");
}

/* Save functions for UI input */
static void input_ui_save_all(struct option_widget *wgt, void *data)
{
    (void)wgt;
    (void)data;
    config_save_ui_input(config_get_global_table(), input_ui_mask);
    input_ui_mask = 0;
    guitop_show_saved_toast("Settings saved.");
}

static void input_ui_save_rom(struct option_widget *wgt, void *data)
{
    (void)wgt;
    (void)data;
    if (!input_require_rom())
        return;

    config_save_ui_input(config_get_rom_table(), input_ui_mask);
    input_ui_mask = 0;
    config_refresh_rom_source_masks();
    guitop_show_toast_msg("Saved for this ROM.");
}

/* Save functions for A800 keys */
static void input_a800_save_all(struct option_widget *wgt, void *data)
{
    t_config *cfg = config_get_global_cfg();

    (void)wgt;
    (void)data;
    config_save_game_input(cfg->general.machine, config_get_global_table(),
        "All", input_player_mask, input_keyboard_mask);
    input_keyboard_mask = 0;
    input_player_mask[0] = 0;
    guitop_show_saved_toast("Settings saved.");
}

static void input_a800_save_rom(struct option_widget *wgt, void *data)
{
    t_config *cfg = config_get_global_cfg();

    (void)wgt;
    (void)data;
    if (!input_require_rom())
        return;

    config_save_game_input(cfg->general.machine, config_get_rom_table(),
        "this", input_player_mask, input_keyboard_mask);
    input_keyboard_mask = 0;
    input_player_mask[0] = 0;
    config_refresh_rom_source_masks();
    guitop_show_toast_msg("Saved for this ROM.");
}

/* Helper function to initialize widget array */
static void init_widget_array(struct option_widget *wid_ary, int menu_prev)
{
    int i;
    for (i = 0; i < MAX_OPTIONS; i++) {
        wid_ary[i].widget_type = WIDGET_TYPE_LABEL;
        wid_ary[i].data_type = WIDGET_DATA_TYPE_INT;
        wid_ary[i].menu_prev = menu_prev;
        wid_ary[i].menu_next = 0;
        wid_ary[i].divider = 0;
        wid_ary[i].mask_value = 0;
        wid_ary[i].p_mask = NULL;
        wid_ary[i].p_source_mask = NULL;
        wid_ary[i].data = NULL;
        wid_ary[i].list_head = NULL;
        wid_ary[i].get_string = NULL;
        wid_ary[i].run_task = NULL;
        wid_ary[i].accept_input = 1;
        wid_ary[i].show_as_disabled = 0;
        wid_ary[i].input_empty_row = 0;
        wid_ary[i].input_player = NULL;
    }
}

/* True when this input is a gamepad D-pad button (gated by D-Pad Enable, not
   Joystick Enable like other controller inputs). */
static int input_raw_is_dpad(e_dev_type device, e_part_type part_type, int part_num)
{
    if (device != DEV_CONTROLLER || part_type != PART_TYPE_BUTTON)
        return 0;

    switch (part_num) {
        case SDL_GAMEPAD_BUTTON_DPAD_UP:
        case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
        case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
        case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
            return 1;
        default:
            return 0;
    }
}

static int input_is_dpad(t_input_common *inp)
{
    return inp &&
           input_raw_is_dpad(inp->device[0], inp->part_type[0],
                             inp->part_num[0]);
}

/* Check if device type is enabled for this player */
static int is_device_enabled(t_player_input *player, e_dev_type dev)
{
    switch (dev) {
        case DEV_KEYBOARD:
            return player->keyboard_enable;
        case DEV_JOYSTICK:
            return player->joy_enable;
        case DEV_MOUSE:
            return player->mouse_enable;
        case DEV_CONTROLLER:
            return player->joy_enable; /* Controllers act as joysticks */
        default:
            return 1;
    }
}

static void input_refresh_joy_slot_types(void)
{
    pc_get_joy_slot_device_types(input_joy_slot_types, MAX_PC_DEVICE);
}

static int input_hardware_available_for_side(t_input_common *input, int side)
{
    e_dev_type dev;
    int slot;

    if (!input)
        return 0;

    dev = input->device[side];
    if (dev != DEV_JOYSTICK && dev != DEV_CONTROLLER)
        return 1;

    slot = input->device_num[side];
    if (slot < 0 || slot >= MAX_PC_DEVICE)
        return 0;

    return input_joy_slot_types[slot] == dev;
}

static int input_hardware_available(t_input_common *input)
{
    if (!input_hardware_available_for_side(input, 0))
        return 0;

    if (input && input->combo &&
        !input_hardware_available_for_side(input, 1))
        return 0;

    return 1;
}

/* Add t_input_common widget, following linked list */
static int add_input_widget(struct option_widget *wid_ary, int idx,
                           t_input_common *input, const char *label,
                           t_player_input *player, unsigned long long *mask,
                           unsigned long long mask_value, int show_disabled,
                           unsigned long long *source_mask)
{
    t_input_common *inp = input;
    t_input_common *head = input;  /* Store the head of the list */
    int added = 0;

    while (inp && idx < MAX_OPTIONS) {
        if (!input_hardware_available(inp)) {
            inp = inp->next;
            continue;
        }

        /* Check if enabled and device type is active */
        int inp_enabled = inp->enable;
        int dev_enabled = (!player ||
                           (input_is_dpad(inp) ? player->dpad_enable
                                               : is_device_enabled(player, inp->device[0])));
        int normally_visible = inp_enabled && dev_enabled;

        if (normally_visible || (show_disabled && (!inp_enabled || !dev_enabled))) {
            wid_ary[idx].widget_type = WIDGET_TYPE_STRING_INPUT;
            wid_ary[idx].data = inp;
            wid_ary[idx].list_head = head;  /* Store list head for delete operations */
            strncpy(wid_ary[idx].label, label, sizeof(wid_ary[idx].label) - 1);
            wid_ary[idx].label[sizeof(wid_ary[idx].label) - 1] = '\0';
            wid_ary[idx].p_mask = mask;
            wid_ary[idx].p_source_mask = source_mask;
            wid_ary[idx].mask_value = mask_value;
            wid_ary[idx].show_as_disabled = normally_visible ? 0 : 1;
            wid_ary[idx].input_empty_row = 0;
            wid_ary[idx].input_player = player;
            idx++;
            added = 1;
        }
        inp = inp->next;
    }

    if (player && !added && idx < MAX_OPTIONS) {
        wid_ary[idx].widget_type = WIDGET_TYPE_STRING_INPUT;
        wid_ary[idx].data = NULL;
        wid_ary[idx].list_head = head;
        strncpy(wid_ary[idx].label, label, sizeof(wid_ary[idx].label) - 1);
        wid_ary[idx].label[sizeof(wid_ary[idx].label) - 1] = '\0';
        wid_ary[idx].p_mask = mask;
        wid_ary[idx].p_source_mask = source_mask;
        wid_ary[idx].mask_value = mask_value;
        wid_ary[idx].show_as_disabled = 0;
        wid_ary[idx].input_empty_row = 1;
        wid_ary[idx].input_player = player;
        idx++;
    }

    return idx;
}

/* Placeholder run_task for input editing (to be implemented later) */
static void input_edit_task(struct option_widget *wgt, void *data)
{
    /* TODO: Implement input editing modal */
}

int menu_options_input(struct option_widget *wid_ary)
{
    t_config *cfg = config_get_global_cfg();
    int i;
    static const int player_menus[] = {
        KAT_MENU_OPTIONS_INPUT_P1,
        KAT_MENU_OPTIONS_INPUT_P2,
        KAT_MENU_OPTIONS_INPUT_P3,
        KAT_MENU_OPTIONS_INPUT_P4
    };

    init_widget_array(wid_ary, KAT_MENU_MAIN);

    i = -1;

    /* Only show players whose controller type is not NONE */
    for (int p = 0; p < MAX_CONTROLLER; p++) {
        if (cfg->input.game.players[p].type != CTRLR_TYPE_NONE) {
            wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
            snprintf(wid_ary[i].label, sizeof(wid_ary[i].label), "Player %d", p + 1);
            wid_ary[i].menu_next = player_menus[p];
        }
    }

    /* UI */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "UI");
    wid_ary[i].menu_next = KAT_MENU_OPTIONS_INPUT_UI;

    /* A800 Keys - only show if machine is 800 or XL */
    if (cfg->general.machine == MACHINE_TYPE_800 ||
        cfg->general.machine == MACHINE_TYPE_XL) {
        wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
        strcpy(wid_ary[i].label, "A800 Keys");
        wid_ary[i].menu_next = KAT_MENU_OPTIONS_INPUT_A800_KEYS;
    }

    return i + 1;
}

int menu_options_input_player(struct option_widget *wid_ary, int player_num)
{
    t_config *cfg = config_get_global_cfg();
    t_player_input *player = &cfg->input.game.players[player_num];
    unsigned long long *mask = &input_player_mask[player_num];
    unsigned long long *source_mask = &cfg->input_mask[player_num];
    int i;
    char label[48];
    char keypad_name[32];

    init_widget_array(wid_ary, KAT_MENU_OPTIONS_INPUT);
    input_refresh_joy_slot_types();

    i = -1;

    /* Enable toggles */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Joystick Enable");
    wid_ary[i].data = &player->joy_enable;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_JOY_EN;

    /* Use Specific Joystick — only visible when joystick is enabled */
    if (player->joy_enable) {
        wid_ary[++i].widget_type = WIDGET_TYPE_STRING;
        strcpy(wid_ary[i].label, "Use Specific Joystick");
        wid_ary[i].data = (void *)(intptr_t)player_num;
        wid_ary[i].data_type = WIDGET_DATA_TYPE_INT;
        wid_ary[i].p_source_mask = source_mask;
        wid_ary[i].mask_value = KCFG_INP_JOY_GUID | KCFG_INP_JOY_SERIAL;
        wid_ary[i].get_string = input_joy_name_string;
        wid_ary[i].run_task = input_joy_select_setup;
        wid_ary[i].menu_next = KAT_MENU_OPTIONS_INPUT_JOY_SELECT;
    }

    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "D-Pad Enable");
    wid_ary[i].data = &player->dpad_enable;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_DPAD_EN;

    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Keyboard Enable");
    wid_ary[i].data = &player->keyboard_enable;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_KEYBD_EN;

    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Mouse Enable");
    wid_ary[i].data = &player->mouse_enable;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_MOUSE_EN;

    /* Mouse settings - only visible when mouse input is enabled */
    if (player->mouse_enable) {
        wid_ary[++i].widget_type = WIDGET_TYPE_SLIDER;
        strcpy(wid_ary[i].label, "Mouse Speed");
        wid_ary[i].data = &player->mouse_speed;
        wid_ary[i].min = 1;
        wid_ary[i].max = 5;
        wid_ary[i].p_mask = mask;
        wid_ary[i].p_source_mask = source_mask;
        wid_ary[i].mask_value = KCFG_INP_MOUSE_SPD;

        wid_ary[++i].widget_type = WIDGET_TYPE_SELECT;
        strcpy(wid_ary[i].label, "Mouse Optimize for");
        wid_ary[i].min = MOUSE_OPTIMIZE_PADDLES;
        wid_ary[i].max = MOUSE_OPTIMIZE_CURSOR;
        wid_ary[i].data = &player->mouse_optimize;
        wid_ary[i].p_mask = mask;
        wid_ary[i].p_source_mask = source_mask;
        wid_ary[i].mask_value = KCFG_INP_MOUSE_OPT;
        wid_ary[i].get_string = input_get_mouse_optimize_string;
        wid_ary[i].run_task = NULL;
    }

    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Auto Fire");
    wid_ary[i].data = &player->auto_fire;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_AUTO_FIRE;

    if (player->auto_fire) {
        wid_ary[++i].widget_type = WIDGET_TYPE_SLIDER;
        strcpy(wid_ary[i].label, "Auto Fire Speed");
        wid_ary[i].data = &player->auto_fire_speed;
        wid_ary[i].min = 1;
        wid_ary[i].max = 5;
        wid_ary[i].p_mask = mask;
        wid_ary[i].p_source_mask = source_mask;
        wid_ary[i].mask_value = KCFG_INP_AUTO_FIRE_SPD;
    }

    wid_ary[++i].widget_type = WIDGET_TYPE_SLIDER;
    strcpy(wid_ary[i].label, "Joy Deadzone");
    wid_ary[i].data = &player->joy_deadzone;
    wid_ary[i].min = 0;
    wid_ary[i].max = 100;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_JOY_DEAD;

    wid_ary[++i].widget_type = WIDGET_TYPE_SLIDER;
    strcpy(wid_ary[i].label, "Joy Sensitivity");
    wid_ary[i].data = &player->joy_sensitivity;
    wid_ary[i].min = 0;
    wid_ary[i].max = 100;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_JOY_SENS;

    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Analog To Digital");
    wid_ary[i].data = &player->analog_stick_acts_digital;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_SIM_AN;

    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Digital To Analog Enable");
    wid_ary[i].data = &player->digdir_analog_enable;
    wid_ary[i].p_mask = mask;
    wid_ary[i].p_source_mask = source_mask;
    wid_ary[i].mask_value = KCFG_INP_DPAD_AN_EN;

    if (player->digdir_analog_enable) {
        wid_ary[++i].widget_type = WIDGET_TYPE_SLIDER;
        strcpy(wid_ary[i].label, "Digital To Analog Speed");
        wid_ary[i].data = &player->digdir_analog_speed;
        wid_ary[i].min = 1;
        wid_ary[i].max = 10;
        wid_ary[i].p_mask = mask;
        wid_ary[i].p_source_mask = source_mask;
        wid_ary[i].mask_value = KCFG_INP_DPAD_AN_SPD;
    }

    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Show Disabled");
    wid_ary[i].data = &player->show_disabled;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].mask_value = 0;

    wid_ary[i+1].divider = 1;

    /* Controller-type specific inputs */
    if (player->type == CTRLR_TYPE_JOYSTICK) {
        /* Stick directions - divider before first item */
        i = add_input_widget(wid_ary, ++i, &player->stick[DIR_LEFT], "Stick Left",
                            player, mask, KCFG_INP_JOY_LEFT, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->stick[DIR_RIGHT], "Stick Right",
                            player, mask, KCFG_INP_JOY_RIGHT, player->show_disabled, source_mask);
        i--;
        //wid_ary[i].divider = 1;

        i = add_input_widget(wid_ary, ++i, &player->stick[DIR_UP], "Stick Up",
                            player, mask, KCFG_INP_JOY_UP, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->stick[DIR_DOWN], "Stick Down",
                            player, mask, KCFG_INP_JOY_DOWN, player->show_disabled, source_mask);
        i--;
    }

    if (player->type == CTRLR_TYPE_TRACKBALL) {
        /* Trackball directions - divider before first item */
        i = add_input_widget(wid_ary, ++i, &player->trackball[DIR_LEFT], "Trackball Left",
                            player, mask, KCFG_INP_TRK_LEFT, player->show_disabled, source_mask);
        i--;
        //wid_ary[i].divider = 1;

        i = add_input_widget(wid_ary, ++i, &player->trackball[DIR_RIGHT], "Trackball Right",
                            player, mask, KCFG_INP_TRK_RIGHT, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->trackball[DIR_UP], "Trackball Up",
                            player, mask, KCFG_INP_TRK_UP, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->trackball[DIR_DOWN], "Trackball Down",
                            player, mask, KCFG_INP_TRK_DOWN, player->show_disabled, source_mask);
        i--;
    }

    if (player->type == CTRLR_TYPE_PADDLES) {
        /* Paddle 1 - divider before first item */
        i = add_input_widget(wid_ary, ++i, &player->paddles[0].left, "Paddle 1 Left",
                            player, mask, KCFG_INP_PADD_1_LEFT, player->show_disabled, source_mask);
        i--;
        //wid_ary[i].divider = 1;

        i = add_input_widget(wid_ary, ++i, &player->paddles[0].right, "Paddle 1 Right",
                            player, mask, KCFG_INP_PADD_1_RIGHT, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->paddles[0].button, "Paddle 1 Button",
                            player, mask, KCFG_INP_PADD_1_BUTN, player->show_disabled, source_mask);
        i--;

        /* Paddle 2 */
        i = add_input_widget(wid_ary, ++i, &player->paddles[1].left, "Paddle 2 Left",
                            player, mask, KCFG_INP_PADD_2_LEFT, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->paddles[1].right, "Paddle 2 Right",
                            player, mask, KCFG_INP_PADD_2_RIGHT, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->paddles[1].button, "Paddle 2 Button",
                            player, mask, KCFG_INP_PADD_2_BUTN, player->show_disabled, source_mask);
        i--;
    }

    /* Common buttons (always show if enabled) */
    i = add_input_widget(wid_ary, ++i, &player->top_button, "Top Button",
                        player, mask, KCFG_INP_JOY_TOP, player->show_disabled, source_mask);
    i--;

    i = add_input_widget(wid_ary, ++i, &player->bottom_button, "Bottom Button",
                        player, mask, KCFG_INP_JOY_BTM, player->show_disabled, source_mask);
    i--;

    /* Keypad keys (for 5200 with joystick or trackball - the 5200 controllers have built-in keypad) */
    if (cfg->general.machine == MACHINE_TYPE_5200 &&
        (player->type == CTRLR_TYPE_JOYSTICK || player->type == CTRLR_TYPE_TRACKBALL)) {

        /* Keys 0-9 - divider before first keypad item.
         * keypad[] is indexed by KEY_x>>1 (POKEY key code shifted), not sequentially. */
        static const int digit_keypad_idx[10] = {
            KEY_0>>1, KEY_1>>1, KEY_2>>1, KEY_3>>1, KEY_4>>1,
            KEY_5>>1, KEY_6>>1, KEY_7>>1, KEY_8>>1, KEY_9>>1
        };
        int first_keypad = 1;
        for (int k = 0; k < 10; k++) {
            snprintf(label, sizeof(label), "Keypad %d", k);
            int start = ++i;
            i = add_input_widget(wid_ary, start, &player->keypad[digit_keypad_idx[k]], label,
                                player, mask, KCFG_INP_JOY_KEY_0 << k,
                                player->show_disabled, source_mask);
            i--;
            if (first_keypad && i >= start) {
                wid_ary[start].divider = 1;
                first_keypad = 0;
            }
        }

        /* Special keys - also indexed by KEY_x>>1 */
        i = add_input_widget(wid_ary, ++i, &player->keypad[KEY_START>>1], "Keypad Start",
                            player, mask, KCFG_INP_JOY_KEY_STT, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->keypad[KEY_PAUSE>>1], "Keypad Pause",
                            player, mask, KCFG_INP_JOY_KEY_PAU, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->keypad[KEY_RESET>>1], "Keypad Reset",
                            player, mask, KCFG_INP_JOY_KEY_RST, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->keypad[KEY_STAR>>1], "Keypad Star",
                            player, mask, KCFG_INP_JOY_KEY_STR, player->show_disabled, source_mask);
        i--;

        i = add_input_widget(wid_ary, ++i, &player->keypad[KEY_POUND>>1], "Keypad Pound",
                            player, mask, KCFG_INP_JOY_KEY_PND, player->show_disabled, source_mask);
        i--;
    }

    /* Save - divider before this item */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "  Save");
    wid_ary[i].data = (void *)(intptr_t)player_num;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].run_task = input_player_save_all;
    wid_ary[i].divider = 1;

    /* Save for ROM */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "  Save for ROM only [*]");
    wid_ary[i].data = (void *)(intptr_t)player_num;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].run_task = input_player_save_rom;

    return i + 1;
}

int menu_options_input_joy_select(struct option_widget *wid_ary)
{
    t_config *cfg = config_get_global_cfg();
    int i;
    int valid_count = pc_get_valid_joy_count();

    /* Clamp selected_index in case a joystick was removed since we last entered */
    if (valid_count == 0)
        joy_select_ctx.selected_index = 0;
    else if (joy_select_ctx.selected_index >= valid_count)
        joy_select_ctx.selected_index = valid_count - 1;

    unsigned int joy_id = pc_get_valid_joy_id(joy_select_ctx.selected_index);

    static char id_str[32];
    static char type_str[16];
    static char name_str[128];
    static char serial_str[64];

    init_widget_array(wid_ary, joy_select_ctx.prev_menu);
    i = -1;

    /* Joystick SELECT */
    wid_ary[++i].widget_type = WIDGET_TYPE_SELECT;
    strcpy(wid_ary[i].label, "Joystick");
    wid_ary[i].data = &joy_select_ctx.selected_index;
    wid_ary[i].data_type = WIDGET_DATA_TYPE_INT;
    wid_ary[i].min = 0;
    wid_ary[i].max = (valid_count > 0) ? (valid_count - 1) : 0;
    wid_ary[i].p_source_mask = &cfg->input_mask[joy_select_ctx.player_num];
    wid_ary[i].mask_value = KCFG_INP_JOY_GUID | KCFG_INP_JOY_SERIAL;
    wid_ary[i].get_string = joy_select_index_string;
    if (!valid_count) wid_ary[i].accept_input = 0;

    /* ID */
    if (joy_id)
        snprintf(id_str, sizeof(id_str), "%u", joy_id);
    else
        id_str[0] = '\0';
    wid_ary[++i].widget_type = WIDGET_TYPE_STRING;
    strcpy(wid_ary[i].label, "ID");
    wid_ary[i].data = id_str;
    wid_ary[i].data_type = WIDGET_DATA_TYPE_STRING;
    wid_ary[i].accept_input = 0;

    /* Type */
    if (joy_id)
        strncpy(type_str, SDL_IsGamepad(joy_id) ? "Gamepad" : "Joystick", sizeof(type_str) - 1);
    else
        type_str[0] = '\0';
    wid_ary[++i].widget_type = WIDGET_TYPE_STRING;
    strcpy(wid_ary[i].label, "Type");
    wid_ary[i].data = type_str;
    wid_ary[i].data_type = WIDGET_DATA_TYPE_STRING;
    wid_ary[i].accept_input = 0;

    /* Name */
    if (joy_id) {
        const char *n = SDL_GetJoystickNameForID(joy_id);
        strncpy(name_str, n ? n : "", sizeof(name_str) - 1);
        name_str[sizeof(name_str) - 1] = '\0';
    } else {
        name_str[0] = '\0';
    }
    wid_ary[++i].widget_type = WIDGET_TYPE_STRING;
    strcpy(wid_ary[i].label, "Name");
    wid_ary[i].data = name_str;
    wid_ary[i].data_type = WIDGET_DATA_TYPE_STRING;
    wid_ary[i].accept_input = 0;

    /* S/N */
    pc_get_joy_serial(joy_id, serial_str, sizeof(serial_str));
    wid_ary[++i].widget_type = WIDGET_TYPE_STRING;
    strcpy(wid_ary[i].label, "S/N");
    wid_ary[i].data = serial_str;
    wid_ary[i].data_type = WIDGET_DATA_TYPE_STRING;
    wid_ary[i].accept_input = 0;

    /* Divider + Save */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "  Save");
    wid_ary[i].run_task = joy_select_save_all;
    wid_ary[i].divider = 1;
    wid_ary[i].menu_next = joy_select_ctx.prev_menu;

    /* Save for ROM */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "  Save for ROM only [*]");
    wid_ary[i].run_task = joy_select_save_rom;
    wid_ary[i].menu_next = joy_select_ctx.prev_menu;

    return i + 1;
}

int menu_options_input_ui(struct option_widget *wid_ary)
{
    t_config *cfg = config_get_global_cfg();
    t_ui_input *ui = &cfg->input.ui;
    int i;
    char label[48];

    init_widget_array(wid_ary, KAT_MENU_OPTIONS_INPUT);
    input_refresh_joy_slot_types();

    i = -1;

    /* Basic UI inputs */
    i = add_input_widget(wid_ary, ++i, &ui->exit, "Exit",
                        NULL, &input_ui_mask, KCFG_UI_EXIT, 0, NULL);
    i--;

    i = add_input_widget(wid_ary, ++i, &ui->back, "Back",
                        NULL, &input_ui_mask, KCFG_UI_BACK, 0, NULL);
    i--;

    i = add_input_widget(wid_ary, ++i, &ui->fullscreen, "Fullscreen",
                        NULL, &input_ui_mask, KCFG_UI_FULLSCREEN, 0, NULL);
    i--;

    i = add_input_widget(wid_ary, ++i, &ui->screenshot, "Screenshot",
                        NULL, &input_ui_mask, KCFG_UI_SCREENSHOT, 0, NULL);
    i--;

    i = add_input_widget(wid_ary, ++i, &ui->debugger, "Debugger",
                        NULL, &input_ui_mask, KCFG_UI_DEBUGGER, 0, NULL);
    i--;

    /* State Load 1-9 - divider before first item */
    for (int s = 0; s < MAX_UI_STATES; s++) {
        snprintf(label, sizeof(label), "State Load %d", s + 1);
        i = add_input_widget(wid_ary, ++i, &ui->state_load[s], label,
                            NULL, &input_ui_mask, KCFG_UI_LOAD_1 << s, 0, NULL);
        i--;
        if (s == 0)
            wid_ary[i].divider = 1;
    }

    /* State Save 1-9 - divider before first item */
    for (int s = 0; s < MAX_UI_STATES; s++) {
        snprintf(label, sizeof(label), "State Save %d", s + 1);
        i = add_input_widget(wid_ary, ++i, &ui->state_save[s], label,
                            NULL, &input_ui_mask, KCFG_UI_SAVE_1 << s, 0, NULL);
        i--;
        if (s == 0)
            wid_ary[i].divider = 1;
    }

    /* Save - divider before this item */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "  Save");
    wid_ary[i].data = NULL;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].run_task = input_ui_save_all;
    wid_ary[i].divider = 1;

    /* Save for ROM */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "  Save for ROM only [*]");
    wid_ary[i].data = NULL;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].run_task = input_ui_save_rom;

    return i + 1;
}

/* Helper to get human-friendly A800 key label */
static void get_a800_key_label(int key, char *label, int label_size)
{
    char key_string[32];
    input_get_key_string(key, key_string);

    /* Convert key_x format to friendly name */
    if (strlen(key_string) > 4 && strncmp(key_string, "key_", 4) == 0) {
        const char *name = key_string + 4;

        /* Special key names */
        if (strcmp(name, "return") == 0)
            snprintf(label, label_size, "Return");
        else if (strcmp(name, "bksp") == 0)
            snprintf(label, label_size, "Backspace");
        else if (strcmp(name, "semi") == 0)
            snprintf(label, label_size, "Semicolon");
        else if (strcmp(name, "comma") == 0)
            snprintf(label, label_size, "Comma");
        else if (strcmp(name, "period") == 0)
            snprintf(label, label_size, "Period");
        else if (strcmp(name, "slash") == 0)
            snprintf(label, label_size, "Slash");
        else if (strcmp(name, "esc") == 0)
            snprintf(label, label_size, "Escape");
        else if (strcmp(name, "tab") == 0)
            snprintf(label, label_size, "Tab");
        else if (strcmp(name, "caps") == 0)
            snprintf(label, label_size, "Caps Lock");
        else if (strcmp(name, "space") == 0)
            snprintf(label, label_size, "Space");
        else if (strcmp(name, "plus") == 0)
            snprintf(label, label_size, "Plus");
        else if (strcmp(name, "minus") == 0)
            snprintf(label, label_size, "Minus");
        else if (strcmp(name, "star") == 0)
            snprintf(label, label_size, "Asterisk");
        else if (strcmp(name, "equals") == 0)
            snprintf(label, label_size, "Equals");
        else if (strcmp(name, "lesst") == 0)
            snprintf(label, label_size, "Less Than");
        else if (strcmp(name, "moret") == 0)
            snprintf(label, label_size, "Greater Than");
        else if (strcmp(name, "fuji") == 0)
            snprintf(label, label_size, "Fuji/Inverse");
        else if (strcmp(name, "help") == 0)
            snprintf(label, label_size, "Help");
        else if (strlen(name) == 1) {
            /* Single character - uppercase it */
            label[0] = name[0];
            if (label[0] >= 'a' && label[0] <= 'z')
                label[0] = label[0] - 'a' + 'A';
            label[1] = '\0';
        } else {
            /* Capitalize first letter */
            strncpy(label, name, label_size - 1);
            label[label_size - 1] = '\0';
            if (label[0] >= 'a' && label[0] <= 'z')
                label[0] = label[0] - 'a' + 'A';
        }
    } else if (strlen(key_string) == 0) {
        snprintf(label, label_size, "Key %d", key);
    } else {
        strncpy(label, key_string, label_size - 1);
        label[label_size - 1] = '\0';
    }
}

int menu_options_input_a800_keys(struct option_widget *wid_ary)
{
    t_config *cfg = config_get_global_cfg();
    t_game_input *game = &cfg->input.game;
    unsigned long long *mask = &input_keyboard_mask;
    int i;
    char label[48];
    char key_string[32];

    init_widget_array(wid_ary, KAT_MENU_OPTIONS_INPUT);
    input_refresh_joy_slot_types();

    i = -1;

    /* Keyboard keys 0-63 */
    for (int k = 0; k < MAX_KEYS; k++) {
        input_get_key_string(k, key_string);

        /* Only add if the key has a valid name */
        if (strlen(key_string) > 0) {
            get_a800_key_label(k, label, sizeof(label));

            if (game->keyboard[k].enable) {
                wid_ary[++i].widget_type = WIDGET_TYPE_STRING_INPUT;
                strncpy(wid_ary[i].label, label, sizeof(wid_ary[i].label) - 1);
                wid_ary[i].label[sizeof(wid_ary[i].label) - 1] = '\0';
                wid_ary[i].data = &game->keyboard[k];
                wid_ary[i].list_head = &game->keyboard[k];  /* Each keyboard key is its own list head */
                wid_ary[i].p_mask = mask;
                wid_ary[i].p_source_mask = &cfg->keyboard_mask;
                wid_ary[i].mask_value = (1ULL << k);
            }
        }
    }

    /* Special keys - divider before first item */
    {
        int start = ++i;
        i = add_input_widget(wid_ary, start, &game->ctrl_key, "Ctrl Key",
                            NULL, &input_player_mask[0], KCFG_INP_KEY_CTRL, 0,
                            &cfg->input_mask[0]);
        if (i > start)
            wid_ary[start].divider = 1;
        i--;
    }

    i = add_input_widget(wid_ary, ++i, &game->shift_key, "Shift Key",
                        NULL, &input_player_mask[0], KCFG_INP_KEY_SHIFT, 0,
                        &cfg->input_mask[0]);
    i--;

    i = add_input_widget(wid_ary, ++i, &game->break_key, "Break Key",
                        NULL, &input_player_mask[0], KCFG_INP_KEY_BREAK, 0,
                        &cfg->input_mask[0]);
    i--;

    /* Console keys - divider before first item */
    {
        int start = ++i;
        i = add_input_widget(wid_ary, start, &game->start_key, "Start Key",
                            NULL, &input_player_mask[0], KCFG_INP_KEY_START, 0,
                            &cfg->input_mask[0]);
        if (i > start)
            wid_ary[start].divider = 1;
        i--;
    }

    i = add_input_widget(wid_ary, ++i, &game->select_key, "Select Key",
                        NULL, &input_player_mask[0], KCFG_INP_KEY_SELECT, 0,
                        &cfg->input_mask[0]);
    i--;

    i = add_input_widget(wid_ary, ++i, &game->option_key, "Option Key",
                        NULL, &input_player_mask[0], KCFG_INP_KEY_OPTION, 0,
                        &cfg->input_mask[0]);
    i--;

    i = add_input_widget(wid_ary, ++i, &game->reset_key, "Reset Key",
                        NULL, &input_player_mask[0], KCFG_INP_KEY_RESET, 0,
                        &cfg->input_mask[0]);
    i--;

    /* Save - divider before this item */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "  Save");
    wid_ary[i].data = NULL;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].run_task = input_a800_save_all;
    wid_ary[i].divider = 1;

    /* Save for ROM */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "  Save for ROM only [*]");
    wid_ary[i].data = NULL;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].run_task = input_a800_save_rom;

    return i + 1;
}

/* Helper: Count items in linked list */
static int count_input_list(t_input_common *head)
{
    int count = 0;
    t_input_common *p = head;
    while (p) {
        if (p->enable)
            count++;
        p = p->next;
    }
    return count;
}

/* Resolve a joystick/gamepad instance id to a device slot for capture.
 * pc_get_joystick_index_from_id() returns -1 for a device that isn't in
 * g_joy_ids.  We pass that -1 straight through (no silent fall back to slot 0):
 * input_capture_complete() refuses to persist a binding with an unresolved slot
 * so the user retries rather than getting a bogus slot-0 binding saved to disk. */
static int capture_joy_index(int instance_id)
{
    return pc_get_joystick_index_from_id(instance_id);
}

static int input_matches_captured_input(const t_input_common *input)
{
    if (!input || !input->enable || input->combo)
        return 0;

    return input->device[0] == input_capture_ctx.captured_device &&
           input->device_num[0] == input_capture_ctx.captured_device_num &&
           input->part_type[0] == input_capture_ctx.captured_part_type &&
           input->part_num[0] == input_capture_ctx.captured_part_num &&
           input->direction[0] == input_capture_ctx.captured_direction;
}

static t_input_common *input_capture_find_duplicate(void)
{
    t_input_common *p = input_capture_ctx.list_head;

    while (p) {
        if (input_matches_captured_input(p))
            return p;
        p = p->next;
    }

    return NULL;
}

/* Forward declaration */
static void input_capture_start(int is_add);

/* Action: Change input */
static void input_action_change(struct option_widget *wgt, void *data)
{
    (void)wgt;
    (void)data;
    if (input_action_ctx.input_empty_row || !input_action_ctx.input)
        return;
    input_capture_start(0);  /* 0 = change mode */
}

/* Action: Delete input from linked list */
static void input_action_delete(struct option_widget *wgt, void *data)
{
    (void)wgt;
    (void)data;

    t_input_common *to_delete = input_action_ctx.input;
    t_input_common *head = input_action_ctx.list_head;

    if (!to_delete || !head)
        return;

    /* Don't delete if it's the only enabled item in the list */
    if (count_input_list(head) <= 1)
        return;

    /* Find and remove from linked list */
    if (head == to_delete) {
        /* Deleting the head - just disable it instead of removing.
         * We can't actually remove the head since it's embedded in the player struct. */
        to_delete->enable = 0;
    } else {
        /* Find the previous node */
        t_input_common *prev = head;
        while (prev && prev->next != to_delete) {
            prev = prev->next;
        }
        if (prev && prev->next == to_delete) {
            /* Unlink the node from the list */
            prev->next = to_delete->next;
            /* Disable and clear the unlinked node */
            to_delete->enable = 0;
            to_delete->next = NULL;
        }
    }

    /* Update mask to indicate change */
    if (input_action_ctx.p_mask)
        *(input_action_ctx.p_mask) |= input_action_ctx.mask_value;
}

/* Callback for after input capture modal closes - navigate back to action menu */
static void input_capture_post_show(struct modal_config *modal)
{
    (void)modal;
    /* Return to the action menu so user can see the updated binding in the title */
    guitop_set_menu(KAT_MENU_INPUT_ACTION);
}

/* Start input capture modal */
static void input_capture_start(int is_add)
{
    struct modal_config *modal = guitop_get_modal_cfg();

    input_capture_ctx.state = CAPTURE_WAITING_RELEASE;
    /* Mouse clicks: Nuklear processes both DOWN and UP before the button callback fires,
     * so the mouse button is already released — pending_releases must start at 0.
     * Keyboard/joystick: the key/button IS still held, but their UP events will arrive
     * in the next frame's event processing (before any capture), decrementing as needed.
     * Starting at 0 is correct for all cases: keyboard UP in CAPTURE_WAITING is harmlessly
     * ignored, and joystick/gamepad UP is guarded by had_button_down below. */
    input_capture_ctx.pending_releases = 0;
    input_capture_ctx.had_button_down = 0;
    input_capture_ctx.is_add = is_add;
    input_capture_ctx.target = input_action_ctx.input;
    input_capture_ctx.list_head = input_action_ctx.list_head;
    input_capture_ctx.player = input_action_ctx.player;
    input_capture_ctx.p_mask = input_action_ctx.p_mask;
    input_capture_ctx.mask_value = input_action_ctx.mask_value;
    strncpy(input_capture_ctx.label, input_action_ctx.label, sizeof(input_capture_ctx.label) - 1);
    input_capture_ctx.label[sizeof(input_capture_ctx.label) - 1] = '\0';

    /* Reset captured values */
    input_capture_ctx.captured_device = DEV_KEYBOARD;
    input_capture_ctx.captured_device_num = 0;
    input_capture_ctx.captured_part_type = PART_TYPE_KEY;
    input_capture_ctx.captured_part_num = 0;
    input_capture_ctx.captured_direction = DIR_PRESS;
    input_capture_ctx.captured_key_mod = 0;
    input_capture_ctx.captured_key_sym = 0;

    /* Setup modal */
    modal->modal_type = MODAL_INPUT_CAPTURE;
    modal->modal_active = 1;
    modal->result = 0;
    modal->post_show = input_capture_post_show;
    snprintf(modal->msg, sizeof(modal->msg), "Release all inputs...");
    strncpy(modal->title, "Input Capture", sizeof(modal->title) - 1);
    modal->title[sizeof(modal->title) - 1] = '\0';
    /* No buttons */
    modal->accept_btn_label[0] = '\0';
    modal->cancel_btn_label[0] = '\0';
}

/* Apply captured input to t_input_common */
static void input_capture_apply(t_input_common *input)
{
    input->enable = 1;
    input->device[0] = input_capture_ctx.captured_device;
    input->device_num[0] = input_capture_ctx.captured_device_num;
    input->part_type[0] = input_capture_ctx.captured_part_type;
    input->part_num[0] = input_capture_ctx.captured_part_num;
    input->direction[0] = input_capture_ctx.captured_direction;
    input->combo = 0;

    /* Clear secondary slot */
    input->device[1] = DEV_KEYBOARD;
    input->device_num[1] = 0;
    input->part_type[1] = PART_TYPE_KEY;
    input->part_num[1] = 0;
    input->direction[1] = DIR_PRESS;

    /* Mark as changed */
    if (input_capture_ctx.p_mask)
        *(input_capture_ctx.p_mask) |= input_capture_ctx.mask_value;
}

static int input_capture_enable_player_device(void)
{
    t_player_input *player = input_capture_ctx.player;
    unsigned long long *mask = input_capture_ctx.p_mask;
    int changed = 0;

    if (!player)
        return 0;

    switch (input_capture_ctx.captured_device) {
        case DEV_KEYBOARD:
            if (!player->keyboard_enable) {
                player->keyboard_enable = 1;
                if (mask)
                    *mask |= KCFG_INP_KEYBD_EN;
                changed = 1;
            }
            break;
        case DEV_MOUSE:
            if (!player->mouse_enable) {
                player->mouse_enable = 1;
                if (mask)
                    *mask |= KCFG_INP_MOUSE_EN;
                changed = 1;
            }
            break;
        case DEV_JOYSTICK:
            if (!player->joy_enable) {
                player->joy_enable = 1;
                if (mask)
                    *mask |= KCFG_INP_JOY_EN;
                changed = 1;
            }
            break;
        case DEV_CONTROLLER:
            if (input_raw_is_dpad(input_capture_ctx.captured_device,
                                  input_capture_ctx.captured_part_type,
                                  input_capture_ctx.captured_part_num)) {
                if (!player->dpad_enable) {
                    player->dpad_enable = 1;
                    if (mask)
                        *mask |= KCFG_INP_DPAD_EN;
                    changed = 1;
                }

                /*
                 * Runtime controller button setup still gates DEV_CONTROLLER
                 * through joy_enable, so D-pad captures need both toggles.
                 */
                if (!player->joy_enable) {
                    player->joy_enable = 1;
                    if (mask)
                        *mask |= KCFG_INP_JOY_EN;
                    changed = 1;
                }
            } else if (!player->joy_enable) {
                player->joy_enable = 1;
                if (mask)
                    *mask |= KCFG_INP_JOY_EN;
                changed = 1;
            }
            break;
        default:
            break;
    }

    return changed;
}

/* Complete the input capture */
static void input_capture_complete(void)
{
    struct modal_config *modal = guitop_get_modal_cfg();
    t_input_common *duplicate;
    int device_enable_changed;

    /* Refuse to persist a binding whose device slot couldn't be resolved.
     * capture_joy_index() yields -1 for a device that isn't in g_joy_ids;
     * saving it would silently fall back to slot 0 and write a bogus binding.
     * Leave the capture armed (don't close the modal) so the user can just
     * press again once the device is actually tracked. */
    if ((input_capture_ctx.captured_device == DEV_JOYSTICK ||
         input_capture_ctx.captured_device == DEV_CONTROLLER) &&
        input_capture_ctx.captured_device_num < 0) {
        guitop_show_toast_msg("Controller not detected. Try again.");
        return;
    }

    device_enable_changed = input_capture_enable_player_device();

    if (input_capture_ctx.is_add) {
        duplicate = input_capture_find_duplicate();
        if (duplicate) {
            if (device_enable_changed) {
                input_action_ctx.input = duplicate;
                input_action_ctx.input_empty_row = 0;
            } else {
                guitop_show_toast_msg("Input already exists.");
            }
        } else {
            t_input_common *head = input_capture_ctx.list_head;

            if (head && !head->enable) {
                input_capture_apply(head);
                input_action_ctx.input = head;
                input_action_ctx.input_empty_row = 0;
            } else {
                /* Add: Create new t_input_common and append to list */
                t_input_common *new_input = (t_input_common *)calloc(1, sizeof(t_input_common));
                if (new_input) {
                    input_capture_apply(new_input);

                    /* Append to end of list */
                    t_input_common *p = input_capture_ctx.list_head;
                    while (p && p->next) {
                        p = p->next;
                    }
                    if (p) {
                        p->next = new_input;
                        input_action_ctx.input = new_input;
                        input_action_ctx.input_empty_row = 0;
                    } else {
                        free(new_input);
                    }
                }
            }
        }
    } else {
        /* Change: Update existing input */
        if (input_capture_ctx.target) {
            input_capture_apply(input_capture_ctx.target);
        }
    }

    /* Close modal */
    modal->modal_active = 0;
    input_capture_ctx.state = CAPTURE_IDLE;
}

/* Cancel the input capture */
static void input_capture_cancel(void)
{
    struct modal_config *modal = guitop_get_modal_cfg();
    modal->modal_active = 0;
    input_capture_ctx.state = CAPTURE_IDLE;
}

/* Process SDL event for input capture - returns 1 if input was captured */
int menu_input_capture_process_event(SDL_Event *event)
{
    if (input_capture_ctx.state == CAPTURE_IDLE)
        return 0;

    /* Helper: decrement pending release count (transition happens at frame boundary in show fn) */
    #define RELEASE_ONE() do { \
        if (input_capture_ctx.pending_releases > 0) \
            input_capture_ctx.pending_releases--; \
    } while (0)

    switch (event->type) {
        case SDL_EVENT_KEY_DOWN:
            if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE) {
                /* Additional key pressed while waiting for release — track it */
                input_capture_ctx.pending_releases++;
            } else if (input_capture_ctx.state == CAPTURE_WAITING) {
                /* Store the key, wait for key up to get final modifiers */
                input_capture_ctx.captured_device = DEV_KEYBOARD;
                input_capture_ctx.captured_device_num = 0;
                input_capture_ctx.captured_part_type = PART_TYPE_KEY;
                input_capture_ctx.captured_key_sym = event->key.key > 0xff ?
                    (event->key.key & 0xff) | 0x100 : event->key.key;
                input_capture_ctx.captured_part_num = input_capture_ctx.captured_key_sym;
                input_capture_ctx.captured_direction = DIR_PRESS;
                input_capture_ctx.captured_key_mod = event->key.mod & 0x0fff;
                input_capture_ctx.state = CAPTURE_WAITING_KEY_UP;
            }
            return 1;

        case SDL_EVENT_KEY_UP:
            if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE) {
                int key_sym = event->key.key > 0xff ?
                    (event->key.key & 0xff) | 0x100 : event->key.key;
                if (key_sym == SDLK_ESCAPE) {
                    input_capture_cancel();
                    return 1;
                }
                RELEASE_ONE();
                return 0;  /* Don't consume — let Nuklear see key-up to reset key state */
            } else if (input_capture_ctx.state == CAPTURE_WAITING_KEY_UP) {
                /* Check if Escape to cancel */
                int key_sym = event->key.key > 0xff ?
                    (event->key.key & 0xff) | 0x100 : event->key.key;
                if (key_sym == SDLK_ESCAPE) {
                    input_capture_cancel();
                    return 1;
                }

                /* Key up received, capture is complete */
                input_capture_complete();
            }
            return 1;

        case SDL_EVENT_MOUSE_BUTTON_DOWN:
            if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE)
                input_capture_ctx.pending_releases++;
            return 1;

        case SDL_EVENT_MOUSE_BUTTON_UP:
            if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE) {
                RELEASE_ONE();
            } else if (input_capture_ctx.state == CAPTURE_WAITING) {
                input_capture_ctx.captured_device = DEV_MOUSE;
                input_capture_ctx.captured_device_num = 0;
                input_capture_ctx.captured_part_type = PART_TYPE_BUTTON;
                input_capture_ctx.captured_part_num = event->button.button;
                input_capture_ctx.captured_direction = DIR_PRESS;
                input_capture_complete();
            }
            return 1;

        case SDL_EVENT_MOUSE_MOTION:
            if (input_capture_ctx.state == CAPTURE_WAITING) {
                /* Determine direction based on motion */
                float dx = event->motion.xrel;
                float dy = event->motion.yrel;
                float abs_dx = dx < 0.0f ? -dx : dx;
                float abs_dy = dy < 0.0f ? -dy : dy;

                /* Only capture if there's significant movement */
                if (abs_dx > 20.0f || abs_dy > 20.0f) {
                    input_capture_ctx.captured_device = DEV_MOUSE;
                    input_capture_ctx.captured_device_num = 0;

                    /*
                     * Mouse relative motion is routed through the ball handler
                     * internally, but saved mouse motion bindings use axis
                     * nomenclature: a0/a1 with +/- direction.  left/right/up/down
                     * are Atari-side directions and would all be interpreted as
                     * the negative side by input_setup_from_ball().
                     */
                    input_capture_ctx.captured_part_type = PART_TYPE_AXIS;

                    /* Determine primary axis and sign */
                    if (abs_dx > abs_dy) {
                        input_capture_ctx.captured_part_num = 0;
                        input_capture_ctx.captured_direction = (dx > 0.0f) ? DIR_PLUS : DIR_MINUS;
                    } else {
                        input_capture_ctx.captured_part_num = 1;
                        input_capture_ctx.captured_direction = (dy > 0.0f) ? DIR_PLUS : DIR_MINUS;
                    }
                    input_capture_complete();
                }
            }
            return 1;

        case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
            if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE)
                input_capture_ctx.pending_releases++;
            else if (input_capture_ctx.state == CAPTURE_WAITING)
                input_capture_ctx.had_button_down = 1;
            return 1;

        case SDL_EVENT_JOYSTICK_BUTTON_UP:
            if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE) {
                /* SDL fires both joystick and gamepad events for the same physical gamepad button.
                 * Skip RELEASE_ONE here for gamepads — GAMEPAD_BUTTON_UP will handle it.
                 * For pure (non-gamepad) joysticks, handle it here. */
                if (!SDL_IsGamepad(event->jbutton.which))
                    RELEASE_ONE();
                return 0;  /* Don't consume — let nav handler reset Nuklear key state */
            } else if (input_capture_ctx.state == CAPTURE_WAITING) {
                /* Guard: ignore UP for the button that triggered capture (still held at start) */
                if (input_capture_ctx.had_button_down) {
                    input_capture_ctx.captured_device = DEV_JOYSTICK;
                    input_capture_ctx.captured_device_num = capture_joy_index(event->jbutton.which);
                    input_capture_ctx.captured_part_type = PART_TYPE_BUTTON;
                    input_capture_ctx.captured_part_num = event->jbutton.button;
                    input_capture_ctx.captured_direction = DIR_PRESS;
                    input_capture_complete();
                }
            }
            return 1;

        case SDL_EVENT_JOYSTICK_AXIS_MOTION:
            if (input_capture_ctx.state == CAPTURE_WAITING) {
                /* Only capture significant axis movement */
                if (abs(event->jaxis.value) > 16384) {
                    input_capture_ctx.captured_device = DEV_JOYSTICK;
                    input_capture_ctx.captured_device_num = capture_joy_index(event->jaxis.which);
                    input_capture_ctx.captured_part_type = PART_TYPE_AXIS;
                    input_capture_ctx.captured_part_num = event->jaxis.axis;
                    input_capture_ctx.captured_direction = (event->jaxis.value > 0) ? DIR_PLUS : DIR_MINUS;
                    input_capture_complete();
                }
            }
            return 1;

        case SDL_EVENT_JOYSTICK_HAT_MOTION:
            if (input_capture_ctx.state == CAPTURE_WAITING && event->jhat.value != SDL_HAT_CENTERED) {
                input_capture_ctx.captured_device = DEV_JOYSTICK;
                input_capture_ctx.captured_device_num = capture_joy_index(event->jhat.which);
                input_capture_ctx.captured_part_type = PART_TYPE_HAT;
                input_capture_ctx.captured_part_num = event->jhat.hat;

                /* Convert hat value to direction */
                switch (event->jhat.value) {
                    case SDL_HAT_UP:    input_capture_ctx.captured_direction = DIR_UP; break;
                    case SDL_HAT_DOWN:  input_capture_ctx.captured_direction = DIR_DOWN; break;
                    case SDL_HAT_LEFT:  input_capture_ctx.captured_direction = DIR_LEFT; break;
                    case SDL_HAT_RIGHT: input_capture_ctx.captured_direction = DIR_RIGHT; break;
                    default: return 1; /* Diagonal or centered, ignore */
                }
                input_capture_complete();
            }
            return 1;

        case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
            if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE)
                input_capture_ctx.pending_releases++;
            else if (input_capture_ctx.state == CAPTURE_WAITING)
                input_capture_ctx.had_button_down = 1;
            return 1;

        case SDL_EVENT_GAMEPAD_BUTTON_UP:
            if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE) {
                RELEASE_ONE();
                return 0;  /* Don't consume — let nav handler reset Nuklear key state */
            } else if (input_capture_ctx.state == CAPTURE_WAITING) {
                /* Guard: ignore UP for the button that triggered capture (still held at start) */
                if (input_capture_ctx.had_button_down) {
                    input_capture_ctx.captured_device = DEV_CONTROLLER;
                    input_capture_ctx.captured_device_num = capture_joy_index(event->gbutton.which);
                    input_capture_ctx.captured_part_type = PART_TYPE_BUTTON;
                    input_capture_ctx.captured_part_num = event->gbutton.button;
                    input_capture_ctx.captured_direction = DIR_PRESS;
                    input_capture_complete();
                }
            }
            return 1;

        case SDL_EVENT_GAMEPAD_AXIS_MOTION:
            if (input_capture_ctx.state == CAPTURE_WAITING) {
                if (abs(event->gaxis.value) > 16384) {
                    input_capture_ctx.captured_device = DEV_CONTROLLER;
                    input_capture_ctx.captured_device_num = capture_joy_index(event->gaxis.which);
                    input_capture_ctx.captured_part_type = PART_TYPE_AXIS;
                    input_capture_ctx.captured_part_num = event->gaxis.axis;
                    input_capture_ctx.captured_direction = (event->gaxis.value > 0) ? DIR_PLUS : DIR_MINUS;
                    input_capture_complete();
                }
            }
            return 1;
    }

    return 0;
}

/* Render the input capture modal */
void menu_input_capture_show(struct nk_context *ctx, int screen_w, int screen_h)
{
    struct modal_config *modal = guitop_get_modal_cfg();

    /* Transition from WAITING_RELEASE to WAITING at frame boundary.
     * Doing this here (after all events are processed) ensures any OS-injected
     * keyboard events from the same physical button press are absorbed first. */
    if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE &&
        input_capture_ctx.pending_releases == 0) {
        input_capture_ctx.state = CAPTURE_WAITING;
        snprintf(modal->msg, sizeof(modal->msg), "Provide input for %s", input_capture_ctx.label);
    }

    ctx->style.font = &(g_theme.font_normal->handle);
    theme_update_dimensions(ctx);

    /* Full-screen input blocker + dim background */
    {
        struct nk_style_window *s = &ctx->style.window;
        struct nk_color old_bg = s->background;

        s->background = nk_rgba(0, 0, 0, 120);

        nk_begin(ctx, "__capture_bg",
            nk_rect(0, 0, screen_w, screen_h),
            NK_WINDOW_NO_SCROLLBAR |
            NK_WINDOW_BACKGROUND);

        nk_end(ctx);

        s->background = old_bg;
    }

    /* Modal window */
    struct nk_rect modal_rect =
        nk_rect((screen_w - 420) / 2,
                (screen_h - 180) / 2,
                420, 180);

    if (nk_begin(ctx, modal->title,
        modal_rect,
        NK_WINDOW_BORDER |
        NK_WINDOW_TITLE |
        NK_WINDOW_NO_SCROLLBAR))
    {
        /* Modal: keep it out of NK_WINDOW_ROM so the mouse works even when the
           modal was opened by a mouse click.  See filebrowser_show() for the
           full explanation of the stuck-ROM interaction. */
        ctx->current->flags &= ~(nk_flags)NK_WINDOW_ROM;
        ctx->current->layout->flags &= ~(nk_flags)NK_WINDOW_ROM;

        nk_window_set_focus(ctx, modal->title);

        nk_layout_row_dynamic(ctx, 10, 1);
        nk_spacing(ctx, 1);

        /* Message */
        nk_layout_row_dynamic(ctx, g_theme.row_height * 2, 1);
        nk_label_wrap(ctx, modal->msg);

        nk_layout_row_dynamic(ctx, 10, 1);
        nk_spacing(ctx, 1);

        /* Instructions */
        nk_layout_row_dynamic(ctx, g_theme.row_height, 1);
        if (input_capture_ctx.state == CAPTURE_WAITING_RELEASE) {
            nk_label(ctx, "Release all inputs to continue...", NK_TEXT_CENTERED);
        } else if (input_capture_ctx.state == CAPTURE_WAITING_KEY_UP) {
            nk_label(ctx, "Release key to confirm...", NK_TEXT_CENTERED);
        } else {
            nk_label(ctx, "Press ESC to cancel", NK_TEXT_CENTERED);
        }
    }
    nk_end(ctx);
}

/* Check if input capture is active */
int menu_input_capture_is_active(void)
{
    return input_capture_ctx.state != CAPTURE_IDLE;
}

/* Action: Add new input (placeholder for now) */
static void input_action_add(struct option_widget *wgt, void *data)
{
    (void)wgt;
    (void)data;
    input_capture_start(1);  /* 1 = add mode */
}

/* Setup the input action context before opening the menu */
void menu_input_action_setup(struct option_widget *wgt, int prev_menu)
{
    //printf("menu_input_action_setup: prev_menu=%d wgt->label='%s'\n",
    //       prev_menu, wgt->label);

    input_action_ctx.input = (t_input_common *)wgt->data;
    input_action_ctx.list_head = (t_input_common *)wgt->list_head;
    input_action_ctx.player = (t_player_input *)wgt->input_player;
    input_action_ctx.prev_menu = prev_menu;
    input_action_ctx.input_empty_row = wgt->input_empty_row;
    input_action_ctx.p_mask = wgt->p_mask;
    input_action_ctx.mask_value = wgt->mask_value;
    strncpy(input_action_ctx.label, wgt->label, sizeof(input_action_ctx.label) - 1);
    input_action_ctx.label[sizeof(input_action_ctx.label) - 1] = '\0';
}

/* Get the title for the input action menu */
const char *menu_input_action_get_title(void)
{
    static char title_buf[128];
    char input_str[64];

    if (input_action_ctx.input_empty_row) {
        strncpy(input_str, "Not mapped", sizeof(input_str) - 1);
        input_str[sizeof(input_str) - 1] = '\0';
    } else {
        input_common_to_string(input_action_ctx.input, input_str, sizeof(input_str));
    }
    snprintf(title_buf, sizeof(title_buf), "%s   %s", input_action_ctx.label, input_str);
    return title_buf;
}

/* Build the input action menu */
int menu_input_action(struct option_widget *wid_ary)
{
    int i;
    char add_label[80];
    int can_delete;

    //printf("menu_input_action called: prev_menu=%d label='%s'\n",
    //       input_action_ctx.prev_menu, input_action_ctx.label);

    init_widget_array(wid_ary, input_action_ctx.prev_menu);

    i = -1;

    /* Title is set from menu_set, but we need to use the label */
    /* The menu title will be set in guitop based on context */

    if (!input_action_ctx.input_empty_row) {
        /* Change option - no menu_next, modal handles navigation */
        wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
        strcpy(wid_ary[i].label, "Change");
        wid_ary[i].run_task = input_action_change;
        wid_ary[i].menu_next = 0;

        /* Delete option - only if more than one input in list */
        can_delete = (count_input_list(input_action_ctx.list_head) > 1);
        wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
        strcpy(wid_ary[i].label, "Delete");
        if (can_delete) {
            wid_ary[i].run_task = input_action_delete;
            wid_ary[i].menu_next = input_action_ctx.prev_menu;
        } else {
            /* Disabled - show but don't allow action */
            wid_ary[i].accept_input = 0;
        }
    }

    /* Add option - no menu_next, modal handles navigation */
    snprintf(add_label, sizeof(add_label), "Add Entry for %s", input_action_ctx.label);
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strncpy(wid_ary[i].label, add_label, sizeof(wid_ary[i].label) - 1);
    wid_ary[i].label[sizeof(wid_ary[i].label) - 1] = '\0';
    wid_ary[i].run_task = input_action_add;
    wid_ary[i].menu_next = 0;

    //printf("menu_input_action returning %d items: [0]='%s' [1]='%s' [2]='%s'\n",
    //       i + 1, wid_ary[0].label, wid_ary[1].label, wid_ary[2].label);

    return i + 1;
}
