#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "menu.h"
#include "widget.h"
#include "../interface/katconfig.h"
#include "../interface/media_if.h"
#include "../interface/util.h"
#include "../interface/sdl_if.h"
#include "modal.h"
#include "guitop.h"
#include "theme.h"
#include "filebrowser.h"

extern struct theme g_theme;

static t_media_scan a5200_bios_scan_params = {0};
static t_media_scan a5200_rom_scan_params = {0};
static t_media_scan a8_bios_scan_params = {0};
static t_media_scan a8_rom_scan_params = {0};

#ifdef HAVE_ROM_DOWNLOAD
#include "../minizip/unzip.h"
#include "../interface/logger.h"
#ifdef HAVE_CURL
#include <curl/curl.h>
#endif
#ifdef __ANDROID__
#include <jni.h>
#include <SDL3/SDL.h>      /* SDL_GetAndroidJNIEnv */
#define DL_MAX_ATTEMPTS 4  /* retries over archive.org's flaky storage nodes */
#endif
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <winhttp.h>
#define DL_URL_WMAX 4096   /* max wide chars for a URL / path component */
#define DL_MAX_ATTEMPTS 4  /* retries over archive.org's flaky storage nodes */
#endif
static int g_download_romset = 0;
static t_media_scan a5200_dl_params = {0};
#endif

static int g_dir_set = 0;

static int g_scan_5200_bios = 0;
static int g_scan_5200_rom  = 0;
static int g_scan_a8_bios   = 0;
static int g_scan_a8_rom    = 0;

static unsigned long long mask_setup = 0;

#define SETUP_GAME_PREFIX "    \xe2\x80\xa2 "

/* Global settings for mouse presets and 2-stick games */
int g_enable_mouse_trackball_games = 0;
int g_enable_mouse_joystick_games = 0;
int g_setup_2stick_games = 0;

/* Per-section input style toggles */
static int g_trackball_setup_mouse    = 0;
static int g_pcgames_setup_mouse      = 0;
static int g_pcgames_digital_analog   = 0;

static int g_mouse_trackball_centipede        = 0;
static int g_mouse_trackball_missile_cmd      = 0;
static int g_mouse_trackball_super_breakout   = 0;
static int g_mouse_trackball_bug_hunt         = 0;

static int g_mouse_joystick_star_wars_arcade  = 0;
static int g_mouse_joystick_gorf              = 0;
static int g_mouse_joystick_castle_crises     = 0;
static int g_mouse_joystick_kaboom            = 0;
static int g_mouse_joystick_bug_hunt          = 0;

static int g_2stick_space_dungeon = 0;
static int g_2stick_robotron      = 0;

typedef struct {
    int h_low;
    int h_center;
    int h_high;
    int v_low;
    int v_center;
    int v_high;
    int save_horizontal;
    int save_vertical;
} t_setup_pot_profile;

static const t_setup_pot_profile setup_pots_castle_crises = {
    4, 112, 218, 0, 0, 0, 1, 0
};
static const t_setup_pot_profile setup_pots_star_wars = {
    73, 124, 183, 72, 124, 182, 1, 1
};
static const t_setup_pot_profile setup_pots_kaboom = {
    39, 98, 104, 0, 0, 0, 1, 0
};

static void setup_apply_2stick_profile(unsigned long crc);
static void setup_apply_trackball_profile(unsigned long crc, int mouse_optimize,
                                          int setup_mouse);
static void setup_apply_bug_hunt_trackball_profile(unsigned long crc, int setup_mouse);
static void setup_apply_mouse_joystick_profile(unsigned long crc, int mouse_optimize,
                                          int setup_mouse, int digital_analog,
                                          int castle_crises_mouse,
                                          const t_setup_pot_profile *pot_profile);

void menu_options_setup_exit(void)
{
    free(a5200_bios_scan_params.directory);
    free(a5200_rom_scan_params.directory);
    free(a8_bios_scan_params.directory);
    free(a8_rom_scan_params.directory);
#ifdef HAVE_ROM_DOWNLOAD
    free(a5200_dl_params.directory);
    a5200_dl_params.directory = NULL;
#endif
}

void setup_save_dir_result(struct modal_config *cfg, char **bios_dir, char **rom_dir, char *label)
{
    struct option_widget *wgt;

    if (!cfg->result)
        return;

    mask_setup |= cfg->id;

    switch(cfg->id) {
        case KCFG_MACHINE_BIOS_DIR: util_alloc_string(bios_dir, filebrowser_result()); break;
        case KCFG_MACHINE_ROM_DIR: util_alloc_string(rom_dir, filebrowser_result()); break;
    }

    if (wgt = menu_find_item_by_label(cfg->id, label)) {
        switch(cfg->id) {
            case KCFG_MACHINE_BIOS_DIR: wgt->data = *bios_dir; break;
            case KCFG_MACHINE_ROM_DIR: wgt->data = *rom_dir; break;
        }
    }
}

void setup_save_5200_bit_dir_result(struct modal_config *cfg)
{
    setup_save_dir_result(cfg, &a5200_bios_scan_params.directory, &a5200_rom_scan_params.directory, "5200 ");
}

void setup_save_8_bit_dir_result(struct modal_config *cfg)
{
    setup_save_dir_result(cfg, &a8_bios_scan_params.directory, &a8_rom_scan_params.directory, "8-bit ");
}

void setup_choose_dir(struct option_widget *wgt, void *data, void *result_func)
{
    struct modal_config *cfg = guitop_get_modal_cfg();
    char *tmp = (wgt->mask_value == KCFG_MACHINE_BIOS_DIR  ? "BIOS" : "ROM");
    t_media_scan *scan = (t_media_scan *)data;

    (void)wgt;
    memset(cfg, 0, sizeof(struct modal_config));
    cfg->data = scan ? scan->directory : NULL;
    cfg->modal_type = MODAL_FB_DIR;
    cfg->modal_active = 2;
    cfg->msg_type = 0;
    cfg->id = wgt->mask_value;
    sprintf(cfg->title, "Choose %s Directory", tmp);
    cfg->post_show = result_func;

    filebrowser_init(cfg);
}

void setup_choose_8_bit_dir(struct option_widget *wgt, void *data)
{
    setup_choose_dir(wgt, data, setup_save_8_bit_dir_result);
}

void setup_choose_5200_bit_dir(struct option_widget *wgt, void *data)
{
    setup_choose_dir(wgt, data, setup_save_5200_bit_dir_result);
}

void setup_save_config(struct option_widget *wgt, void *data)
{
    (void)data;
    (void)wgt;

    config_save_general(config_get_global_table(), mask_setup);

    config_save_directories_by_machine(config_get_global_table(), mask_setup, MACHINE_TYPE_5200,
        a5200_rom_scan_params.directory, a5200_bios_scan_params.directory);
    config_save_directories_by_machine(config_get_global_table(), mask_setup, MACHINE_TYPE_800,
        a8_rom_scan_params.directory, a8_bios_scan_params.directory);

    /* config_save_general above already flushed, but it ran before the
     * directory keys existed, and config_save_directories_by_machine only sets
     * them in the in-memory tree.  Without this the chosen ROM/BIOS directories
     * reached disk only via the write at exit, so anything that killed the
     * process first - swiping the app away, a low-memory kill, a crash - threw
     * them away despite the user having pressed Save.  One flush covers both
     * machines. */
    config_write_file(config_get_global_table());

    mask_setup = 0;

    if (g_enable_mouse_trackball_games && g_trackball_setup_mouse) {
        int sm = g_trackball_setup_mouse;
        if (g_mouse_trackball_centipede)
            setup_apply_trackball_profile(0x536a70fe, MOUSE_OPTIMIZE_CURSOR, sm);
        if (g_mouse_trackball_missile_cmd)
            setup_apply_trackball_profile(0x44d3ff6f, MOUSE_OPTIMIZE_CURSOR, sm);
        if (g_mouse_trackball_super_breakout)
            setup_apply_trackball_profile(0xa0642110, MOUSE_OPTIMIZE_PADDLES, sm);
        if (g_mouse_trackball_bug_hunt)
            setup_apply_bug_hunt_trackball_profile(0x7899a017, sm);
    }

    if (g_enable_mouse_joystick_games &&
            (g_pcgames_setup_mouse || g_pcgames_digital_analog)) {
        int sm = g_pcgames_setup_mouse;
        int da = g_pcgames_digital_analog;
        if (g_mouse_joystick_star_wars_arcade)
            setup_apply_mouse_joystick_profile(0x75f566df, MOUSE_OPTIMIZE_CURSOR, sm, da,
                    0, &setup_pots_star_wars);
        if (g_mouse_joystick_gorf)
            setup_apply_mouse_joystick_profile(0xe955db74, MOUSE_OPTIMIZE_CURSOR, sm, da,
                    0, NULL);
        if (g_mouse_joystick_castle_crises)
            setup_apply_mouse_joystick_profile(0xd50e4061, MOUSE_OPTIMIZE_PADDLES, sm, da,
                    1, &setup_pots_castle_crises);
        if (g_mouse_joystick_kaboom)
            setup_apply_mouse_joystick_profile(0x420f5d0b, MOUSE_OPTIMIZE_PADDLES, sm, da,
                    0, &setup_pots_kaboom);
        if (g_mouse_joystick_bug_hunt)
            setup_apply_mouse_joystick_profile(0xd4cd9359, MOUSE_OPTIMIZE_CURSOR, sm, da,
                    0, NULL);
    }

    if (g_setup_2stick_games) {
        if (g_2stick_space_dungeon) setup_apply_2stick_profile(0xb68d61e8);
        if (g_2stick_robotron)      setup_apply_2stick_profile(0x4252abd9);
    }

    guitop_show_saved_toast("Settings saved.");
}

static void setup_make_mouse_input(t_input_common *inp, e_part_type part_type, e_direction direction)
{
    memset(inp, 0, sizeof(t_input_common));
    inp->enable       = 1;
    inp->device[0]    = DEV_MOUSE;
    inp->device_num[0]= 0;

    if (part_type == PART_TYPE_BUTTON) {
        inp->part_type[0] = PART_TYPE_BUTTON;
        inp->part_num[0]  = 1;
        inp->direction[0] = direction;
        return;
    }

    inp->part_type[0] = PART_TYPE_AXIS;
    switch (direction) {
        case DIR_LEFT:
            inp->part_num[0]  = 0;
            inp->direction[0] = DIR_MINUS;
            break;
        case DIR_RIGHT:
            inp->part_num[0]  = 0;
            inp->direction[0] = DIR_PLUS;
            break;
        case DIR_UP:
            inp->part_num[0]  = 1;
            inp->direction[0] = DIR_MINUS;
            break;
        case DIR_DOWN:
            inp->part_num[0]  = 1;
            inp->direction[0] = DIR_PLUS;
            break;
        default:
            inp->part_num[0]  = 0;
            inp->direction[0] = direction;
            break;
    }
}

/* Right thumbstick raw joystick axis numbers — Xbox-style mapping on Linux.
   Adjust if using a different controller type. */
#define JOYSTICK_RIGHTX_AXIS 3
#define JOYSTICK_RIGHTY_AXIS 4

static void setup_make_axis_input(t_input_common *inp, e_dev_type dev,
                                  int dev_num, int axis_num, e_direction direction)
{
    memset(inp, 0, sizeof(t_input_common));
    inp->enable       = 1;
    inp->device[0]    = dev;
    inp->device_num[0]= dev_num;
    inp->part_type[0] = PART_TYPE_AXIS;
    inp->part_num[0]  = axis_num;
    inp->direction[0] = direction;
}

/* Digital button on a raw joystick or SDL gamepad (direction is DIR_PRESS;
   the array slot it is stored in determines what it actuates). */
static void setup_make_button_input(t_input_common *inp, e_dev_type dev,
                                    int dev_num, int button_num)
{
    memset(inp, 0, sizeof(t_input_common));
    inp->enable        = 1;
    inp->device[0]     = dev;
    inp->device_num[0] = dev_num;
    inp->part_type[0]  = PART_TYPE_BUTTON;
    inp->part_num[0]   = button_num;
    inp->direction[0]  = DIR_PRESS;
}

/* Raw joystick POV hat direction (j0|d0|<dir>). The hat handler matches on
   direction[0] (DIR_LEFT/RIGHT/UP/DOWN) to pick the hat mask. */
static void setup_make_hat_input(t_input_common *inp, int dev_num,
                                 int hat_num, e_direction direction)
{
    memset(inp, 0, sizeof(t_input_common));
    inp->enable        = 1;
    inp->device[0]     = DEV_JOYSTICK;
    inp->device_num[0] = dev_num;
    inp->part_type[0]  = PART_TYPE_HAT;
    inp->part_num[0]   = hat_num;
    inp->direction[0]  = direction;
}

/* Append node to the tail of the chain headed by `head`; clears node->next.
   Lets device defaults be added without clobbering an existing chain. */
static void setup_chain_append(t_input_common *head, t_input_common *node)
{
    t_input_common *p = head;
    node->next = NULL;
    while (p->next)
        p = p->next;
    p->next = node;
}

static void setup_apply_2stick_profile(unsigned long crc)
{
    t_config *cfg;
    t_player_input saved_p0, saved_p1;
    t_toml_node *tbl;
    unsigned long long mask[MAX_CONTROLLER];
    char *path;

    /* Two t_input_common nodes per direction: c0 right stick chained to j0 right stick */
    t_input_common c_left,  j_left;
    t_input_common c_right, j_right;
    t_input_common c_up,    j_up;
    t_input_common c_down,  j_down;

    path = media_if_get_profile_from_crc(crc);
    if (!path) return;

    tbl = toml_parse_file(path);
    free(path);
    if (!tbl) return;

    cfg = config_get_global_cfg();
    saved_p0 = cfg->input.game.players[0];
    saved_p1 = cfg->input.game.players[1];

    /* Port 0: enforce Joystick type only, leave bindings untouched */
    cfg->input.game.players[0].type = CTRLR_TYPE_JOYSTICK;
    cfg->input.game.players[0].joy_sensitivity = 100;

    /* Port 1: Joystick type, right thumbstick for all directions.
       Each direction: c0 GameController right stick chained to j0 raw joystick right stick. */
    memset(&cfg->input.game.players[1], 0, sizeof(t_player_input));
    cfg->input.game.players[1].type        = CTRLR_TYPE_JOYSTICK;
    cfg->input.game.players[1].joy_enable  = 1;
    cfg->input.game.players[1].joy_sensitivity = 100;

    setup_make_axis_input(&c_left,  DEV_CONTROLLER, 0, SDL_GAMEPAD_AXIS_RIGHTX, DIR_MINUS);
    setup_make_axis_input(&j_left,  DEV_JOYSTICK,   0, JOYSTICK_RIGHTX_AXIS,    DIR_MINUS);
    c_left.next = &j_left;
    cfg->input.game.players[1].stick[DIR_LEFT] = c_left;

    setup_make_axis_input(&c_right, DEV_CONTROLLER, 0, SDL_GAMEPAD_AXIS_RIGHTX, DIR_PLUS);
    setup_make_axis_input(&j_right, DEV_JOYSTICK,   0, JOYSTICK_RIGHTX_AXIS,    DIR_PLUS);
    c_right.next = &j_right;
    cfg->input.game.players[1].stick[DIR_RIGHT] = c_right;

    setup_make_axis_input(&c_up,   DEV_CONTROLLER, 0, SDL_GAMEPAD_AXIS_RIGHTY, DIR_MINUS);
    setup_make_axis_input(&j_up,   DEV_JOYSTICK,   0, JOYSTICK_RIGHTY_AXIS,    DIR_MINUS);
    c_up.next = &j_up;
    cfg->input.game.players[1].stick[DIR_UP] = c_up;

    setup_make_axis_input(&c_down, DEV_CONTROLLER, 0, SDL_GAMEPAD_AXIS_RIGHTY, DIR_PLUS);
    setup_make_axis_input(&j_down, DEV_JOYSTICK,   0, JOYSTICK_RIGHTY_AXIS,    DIR_PLUS);
    c_down.next = &j_down;
    cfg->input.game.players[1].stick[DIR_DOWN] = c_down;

    memset(mask, 0, sizeof(mask));
    mask[0] = KCFG_INP_TYPE     | KCFG_INP_JOY_SENS;
    mask[1] = KCFG_INP_TYPE     | KCFG_INP_JOY_EN    | KCFG_INP_JOY_SENS |
              KCFG_INP_JOY_LEFT | KCFG_INP_JOY_RIGHT  |
              KCFG_INP_JOY_UP   | KCFG_INP_JOY_DOWN;

    config_save_game_input(MACHINE_TYPE_5200, tbl, "this", mask, 0);

    cfg->input.game.players[0] = saved_p0;
    cfg->input.game.players[1] = saved_p1;
    toml_free_node_tree(tbl);
}

/* Full 5200 defaults into player block (machine forced to 5200), then return.
   Caller must have snapshotted players[]; this memsets and repopulates them. */
static void setup_load_5200_defaults(t_config *cfg, e_machine_type *saved_machine)
{
    *saved_machine = cfg->general.machine;
    memset(cfg->input.game.players, 0, sizeof(cfg->input.game.players));
    cfg->general.machine = MACHINE_TYPE_5200;
    pc_set_controller_defaults();
    cfg->general.machine = *saved_machine;
}

static void setup_save_5200_joystick_pots(t_config *cfg, t_toml_node *tbl,
                                          const t_setup_pot_profile *pots)
{
    unsigned long long mask = 0;
    e_machine_type saved_machine_type;
    t_machine saved_machine;

    if (!pots)
        return;

    saved_machine_type = cfg->general.machine;
    saved_machine = cfg->machine;
    cfg->general.machine = MACHINE_TYPE_5200;

    if (pots->save_horizontal) {
        cfg->machine.joystick_hpot_low = pots->h_low;
        cfg->machine.joystick_hpot_center = pots->h_center;
        cfg->machine.joystick_hpot_high = pots->h_high;
        mask |= KCFG_MACHINE_JOY_H_LOW | KCFG_MACHINE_JOY_H_CTR |
                KCFG_MACHINE_JOY_H_HIGH;
    }

    if (pots->save_vertical) {
        cfg->machine.joystick_vpot_low = pots->v_low;
        cfg->machine.joystick_vpot_center = pots->v_center;
        cfg->machine.joystick_vpot_high = pots->v_high;
        mask |= KCFG_MACHINE_JOY_V_LOW | KCFG_MACHINE_JOY_V_CTR |
                KCFG_MACHINE_JOY_V_HIGH;
    }

    config_save_general(tbl, mask);

    cfg->machine = saved_machine;
    cfg->general.machine = saved_machine_type;
}

static void setup_apply_trackball_profile(unsigned long crc, int mouse_optimize,
                                          int setup_mouse)
{
    t_config *cfg;
    t_player_input saved[MAX_CONTROLLER];
    e_machine_type saved_machine;
    t_toml_node *tbl;
    unsigned long long mask[MAX_CONTROLLER];
    char *path;
    int j;
    t_input_common kb_dir[MAX_DIR];   /* default direction keys chained into trackball[] */
    t_input_common joy_axis[MAX_DIR]; /* raw joystick main axis (j0|a0/a1) */
    t_input_common ctrl_axis[MAX_DIR];/* SDL gamepad left stick (c0|LEFTX/LEFTY) */
    t_input_common joy_start, ctrl_start; /* raw joystick / gamepad Start button */
    t_input_common mouse_btm, mouse_top, mouse_start;

    path = media_if_get_profile_from_crc(crc);
    if (!path) return;

    tbl = toml_parse_file(path);
    free(path);
    if (!tbl) return;

    cfg = config_get_global_cfg();
    for (j = 0; j < MAX_CONTROLLER; ++j)
        saved[j] = cfg->input.game.players[j];

    setup_load_5200_defaults(cfg, &saved_machine);

    /* Keep default direction keys to chain alongside the mouse */
    for (j = DIR_LEFT; j < MAX_DIR; ++j) {
        kb_dir[j] = cfg->input.game.players[0].stick[j];
        kb_dir[j].next = NULL;
    }

    cfg->input.game.players[0].type             = CTRLR_TYPE_TRACKBALL;
    cfg->input.game.players[0].mouse_enable     = setup_mouse;
    cfg->input.game.players[0].mouse_optimize   = mouse_optimize;
    cfg->input.game.players[0].digdir_analog_enable = 0;

    for (j = DIR_LEFT; j < MAX_DIR; ++j) {
        if (setup_mouse) {
            int axis        = (j == DIR_UP || j == DIR_DOWN) ? 1 : 0;
            e_direction sgn = (j == DIR_RIGHT || j == DIR_DOWN) ? DIR_PLUS : DIR_MINUS;
            int gp_axis     = (axis == 0) ? SDL_GAMEPAD_AXIS_LEFTX : SDL_GAMEPAD_AXIS_LEFTY;

            setup_make_mouse_input(&cfg->input.game.players[0].trackball[j],
                                   PART_TYPE_BALL, j);
            cfg->input.game.players[0].trackball[j].next = &kb_dir[j];

            /* Also drive the trackball from the main analog axes of a real
               joystick (axis 0/1) and SDL gamepad (left stick). */
            setup_make_axis_input(&joy_axis[j],  DEV_JOYSTICK,   0, axis,    sgn);
            setup_chain_append(&cfg->input.game.players[0].trackball[j], &joy_axis[j]);
            setup_make_axis_input(&ctrl_axis[j], DEV_CONTROLLER, 0, gp_axis, sgn);
            setup_chain_append(&cfg->input.game.players[0].trackball[j], &ctrl_axis[j]);
        }
        else {
            cfg->input.game.players[0].trackball[j] = kb_dir[j];
        }
    }

    /* The save mask overwrites key_start, so the profile's controller Start
       mapping is lost unless we re-add it here next to the keyboard default. */
    setup_make_button_input(&joy_start,  DEV_JOYSTICK,   0, 7);
    setup_chain_append(&cfg->input.game.players[0].keypad[KEY_START>>1], &joy_start);
    setup_make_button_input(&ctrl_start, DEV_CONTROLLER, 0, SDL_GAMEPAD_BUTTON_START);
    setup_chain_append(&cfg->input.game.players[0].keypad[KEY_START>>1], &ctrl_start);

    /* Mouse buttons are chained onto the keyboard/controller defaults. */
    if (setup_mouse) {
        setup_make_mouse_input(&mouse_btm, PART_TYPE_BUTTON, DIR_PRESS);
        setup_make_mouse_input(&mouse_top, PART_TYPE_BUTTON, DIR_PRESS);
        setup_make_mouse_input(&mouse_start, PART_TYPE_BUTTON, DIR_PRESS);
        mouse_top.part_num[0] = 3;
        mouse_start.part_num[0] = 2;
        cfg->input.game.players[0].bottom_button.next = &mouse_btm;
        cfg->input.game.players[0].top_button.next    = &mouse_top;
        setup_chain_append(&cfg->input.game.players[0].keypad[KEY_START>>1],
                           &mouse_start);
    }

    memset(mask, 0, sizeof(mask));
    mask[0] = KCFG_INP_TYPE     | KCFG_INP_MOUSE_EN  | KCFG_INP_MOUSE_OPT |
              KCFG_INP_JOY_SENS |
              KCFG_INP_KEYBD_EN | KCFG_INP_JOY_EN    |
              KCFG_INP_TRK_LEFT | KCFG_INP_TRK_RIGHT |
              KCFG_INP_TRK_UP   | KCFG_INP_TRK_DOWN  |
              KCFG_INP_JOY_TOP  | KCFG_INP_JOY_BTM   |
              KCFG_INP_JOY_KEY_STT;

    config_save_game_input(MACHINE_TYPE_5200, tbl, "this", mask, 0);

    for (j = 0; j < MAX_CONTROLLER; ++j)
        cfg->input.game.players[j] = saved[j];
    cfg->general.machine = saved_machine;
    toml_free_node_tree(tbl);
}

static void setup_apply_bug_hunt_trackball_profile(unsigned long crc, int setup_mouse)
{
    t_config *cfg;
    t_player_input saved[MAX_CONTROLLER];
    e_machine_type saved_machine;
    t_toml_node *tbl;
    unsigned long long mask[MAX_CONTROLLER];
    char *path;
    int j;
    t_input_common kb_dir[MAX_DIR];   /* player 1 default keys chained into trackball[] */
    t_input_common joy_axis[MAX_DIR]; /* raw joystick main axis (j0|a0/a1) */
    t_input_common ctrl_axis[MAX_DIR];/* SDL gamepad left stick (c0|LEFTX/LEFTY) */
    t_input_common joy_start, ctrl_start; /* raw joystick / gamepad Start button */
    t_input_common mouse_btm, mouse_top, mouse_start;

    path = media_if_get_profile_from_crc(crc);
    if (!path) return;

    tbl = toml_parse_file(path);
    free(path);
    if (!tbl) return;

    cfg = config_get_global_cfg();
    for (j = 0; j < MAX_CONTROLLER; ++j)
        saved[j] = cfg->input.game.players[j];

    setup_load_5200_defaults(cfg, &saved_machine);

    /* Player 0: joystick defaults, mouse off */
    cfg->input.game.players[0].type         = CTRLR_TYPE_JOYSTICK;
    cfg->input.game.players[0].mouse_enable = 0;

    /* Player 1: trackball, keep default direction keys to chain in */
    for (j = DIR_LEFT; j < MAX_DIR; ++j) {
        kb_dir[j] = cfg->input.game.players[1].stick[j];
        kb_dir[j].next = NULL;
    }

    cfg->input.game.players[1].type             = CTRLR_TYPE_TRACKBALL;
    cfg->input.game.players[1].mouse_enable     = setup_mouse;
    cfg->input.game.players[1].mouse_optimize   = MOUSE_OPTIMIZE_CURSOR;
    cfg->input.game.players[1].digdir_analog_enable = 0;

    for (j = DIR_LEFT; j < MAX_DIR; ++j) {
        if (setup_mouse) {
            int axis        = (j == DIR_UP || j == DIR_DOWN) ? 1 : 0;
            e_direction sgn = (j == DIR_RIGHT || j == DIR_DOWN) ? DIR_PLUS : DIR_MINUS;
            int gp_axis     = (axis == 0) ? SDL_GAMEPAD_AXIS_LEFTX : SDL_GAMEPAD_AXIS_LEFTY;

            setup_make_mouse_input(&cfg->input.game.players[1].trackball[j],
                                   PART_TYPE_BALL, j);
            cfg->input.game.players[1].trackball[j].next = &kb_dir[j];

            /* Also drive the trackball from the main analog axes of a real
               joystick (axis 0/1) and SDL gamepad (left stick). */
            setup_make_axis_input(&joy_axis[j],  DEV_JOYSTICK,   0, axis,    sgn);
            setup_chain_append(&cfg->input.game.players[1].trackball[j], &joy_axis[j]);
            setup_make_axis_input(&ctrl_axis[j], DEV_CONTROLLER, 0, gp_axis, sgn);
            setup_chain_append(&cfg->input.game.players[1].trackball[j], &ctrl_axis[j]);
        }
        else {
            cfg->input.game.players[1].trackball[j] = kb_dir[j];
        }
    }

    /* The save mask overwrites key_start, so the profile's controller Start
       mapping is lost unless we re-add it here next to the keyboard default. */
    setup_make_button_input(&joy_start,  DEV_JOYSTICK,   0, 7);
    setup_chain_append(&cfg->input.game.players[1].keypad[KEY_START>>1], &joy_start);
    setup_make_button_input(&ctrl_start, DEV_CONTROLLER, 0, SDL_GAMEPAD_BUTTON_START);
    setup_chain_append(&cfg->input.game.players[1].keypad[KEY_START>>1], &ctrl_start);

    /* Mouse buttons are chained onto the keyboard/controller defaults. */
    if (setup_mouse) {
        setup_make_mouse_input(&mouse_btm, PART_TYPE_BUTTON, DIR_PRESS);
        setup_make_mouse_input(&mouse_top, PART_TYPE_BUTTON, DIR_PRESS);
        setup_make_mouse_input(&mouse_start, PART_TYPE_BUTTON, DIR_PRESS);
        mouse_top.part_num[0] = 3;
        mouse_start.part_num[0] = 2;
        cfg->input.game.players[1].bottom_button.next = &mouse_btm;
        cfg->input.game.players[1].top_button.next    = &mouse_top;
        setup_chain_append(&cfg->input.game.players[1].keypad[KEY_START>>1],
                           &mouse_start);
    }

    memset(mask, 0, sizeof(mask));
    mask[0] = KCFG_INP_TYPE | KCFG_INP_MOUSE_EN | KCFG_INP_JOY_SENS;
    mask[1] = KCFG_INP_TYPE     | KCFG_INP_MOUSE_EN  | KCFG_INP_MOUSE_OPT |
              KCFG_INP_JOY_SENS |
              KCFG_INP_KEYBD_EN | KCFG_INP_JOY_EN    |
              KCFG_INP_TRK_LEFT | KCFG_INP_TRK_RIGHT |
              KCFG_INP_TRK_UP   | KCFG_INP_TRK_DOWN  |
              KCFG_INP_JOY_BTM  | KCFG_INP_JOY_TOP  |
              KCFG_INP_JOY_KEY_STT;

    config_save_game_input(MACHINE_TYPE_5200, tbl, "this", mask, 0);

    for (j = 0; j < MAX_CONTROLLER; ++j)
        cfg->input.game.players[j] = saved[j];
    cfg->general.machine = saved_machine;
    toml_free_node_tree(tbl);
}

static void setup_apply_mouse_joystick_profile(unsigned long crc, int mouse_optimize,
                                          int setup_mouse, int digital_analog,
                                          int castle_crises_mouse,
                                          const t_setup_pot_profile *pot_profile)
{
    t_config *cfg;
    t_player_input saved[MAX_CONTROLLER];
    e_machine_type saved_machine;
    t_toml_node *tbl;
    unsigned long long mask[MAX_CONTROLLER];
    char *path;
    int j;
    /* Per-direction device nodes chained onto the default keyboard stick[].
       All live until config_save_game_input() below. */
    t_input_common joy_axis[MAX_DIR];    /* raw joystick analog axis (j0|a0/a1) */
    t_input_common joy_hat[MAX_DIR];     /* raw joystick POV hat (j0|d0|<dir>)  */
    t_input_common ctrl_axis[MAX_DIR];   /* SDL gamepad left stick (c0|a_left*) */
    t_input_common ctrl_dpad[MAX_DIR];   /* SDL gamepad D-pad button (c0|b_dpad)*/
    t_input_common mouse_dir[MAX_DIR];   /* mouse axes chained onto stick[]     */
    t_input_common mouse_castle_left, mouse_castle_right;
    t_input_common joy_btn, ctrl_btn;    /* raw joystick / gamepad fire buttons */
    t_input_common joy_start, ctrl_start;/* raw joystick / gamepad Start button */
    t_input_common mouse_btm, mouse_top, mouse_start;
    t_input_common *head;

    path = media_if_get_profile_from_crc(crc);
    if (!path) return;

    tbl = toml_parse_file(path);
    free(path);
    if (!tbl) return;

    cfg = config_get_global_cfg();
    for (j = 0; j < MAX_CONTROLLER; ++j)
        saved[j] = cfg->input.game.players[j];

    setup_load_5200_defaults(cfg, &saved_machine);

    cfg->input.game.players[0].type             = CTRLR_TYPE_JOYSTICK;
    cfg->input.game.players[0].mouse_enable     = setup_mouse;
    cfg->input.game.players[0].mouse_optimize   = mouse_optimize;
    cfg->input.game.players[0].digdir_analog_enable = digital_analog;

    /* Keep the default keyboard arrows as the chain head in stick[], then
       append normal joystick/gamepad controls so generated 5200 profiles work
       with a real controller, not just keyboard/mouse.

       Per direction: raw joystick analog axis + POV hat, then SDL gamepad left
       stick axis + D-pad button. The analog axes drive the 5200 pot directly;
       the digital hat/D-pad entries move the analog stick when digital-to-analog
       is enabled. Mouse axes (if Setup Mouse) are appended last. */
    for (j = DIR_LEFT; j < MAX_DIR; ++j) {
        int axis        = (j == DIR_UP || j == DIR_DOWN) ? 1 : 0;
        e_direction sgn = (j == DIR_RIGHT || j == DIR_DOWN) ? DIR_PLUS : DIR_MINUS;
        int gp_axis     = (axis == 0) ? SDL_GAMEPAD_AXIS_LEFTX : SDL_GAMEPAD_AXIS_LEFTY;
        int dpad_btn;

        switch (j) {
            case DIR_LEFT:  dpad_btn = SDL_GAMEPAD_BUTTON_DPAD_LEFT;  break;
            case DIR_RIGHT: dpad_btn = SDL_GAMEPAD_BUTTON_DPAD_RIGHT; break;
            case DIR_UP:    dpad_btn = SDL_GAMEPAD_BUTTON_DPAD_UP;    break;
            default:        dpad_btn = SDL_GAMEPAD_BUTTON_DPAD_DOWN;  break;
        }

        head = &cfg->input.game.players[0].stick[j];

        setup_make_axis_input(&joy_axis[j], DEV_JOYSTICK, 0, axis, sgn);
        setup_chain_append(head, &joy_axis[j]);

        setup_make_hat_input(&joy_hat[j], 0, 0, j);
        setup_chain_append(head, &joy_hat[j]);

        setup_make_axis_input(&ctrl_axis[j], DEV_CONTROLLER, 0, gp_axis, sgn);
        setup_chain_append(head, &ctrl_axis[j]);

        setup_make_button_input(&ctrl_dpad[j], DEV_CONTROLLER, 0, dpad_btn);
        setup_chain_append(head, &ctrl_dpad[j]);

        if (setup_mouse && (!castle_crises_mouse || (j != DIR_UP && j != DIR_DOWN))) {
            setup_make_mouse_input(&mouse_dir[j], PART_TYPE_AXIS, j);
            setup_chain_append(head, &mouse_dir[j]);
        }
    }

    if (setup_mouse && castle_crises_mouse) {
        setup_make_mouse_input(&mouse_castle_left, PART_TYPE_AXIS, DIR_UP);
        setup_chain_append(&cfg->input.game.players[0].stick[DIR_LEFT],
                           &mouse_castle_left);

        setup_make_mouse_input(&mouse_castle_right, PART_TYPE_AXIS, DIR_DOWN);
        setup_chain_append(&cfg->input.game.players[0].stick[DIR_RIGHT],
                           &mouse_castle_right);
    }

    /* Bottom button: keyboard default head, then raw joystick button 0 and
       gamepad A/South; mouse left button (button 1) last when Setup Mouse on.
       Top/Start keep their defaults and gain mouse right/middle buttons. */
    head = &cfg->input.game.players[0].bottom_button;
    setup_make_button_input(&joy_btn, DEV_JOYSTICK, 0, 0);
    setup_chain_append(head, &joy_btn);
    setup_make_button_input(&ctrl_btn, DEV_CONTROLLER, 0, SDL_GAMEPAD_BUTTON_SOUTH);
    setup_chain_append(head, &ctrl_btn);

    /* The save mask overwrites key_start, so the profile's controller Start
       mapping is lost unless we re-add it here next to the keyboard default. */
    setup_make_button_input(&joy_start,  DEV_JOYSTICK,   0, 7);
    setup_chain_append(&cfg->input.game.players[0].keypad[KEY_START>>1], &joy_start);
    setup_make_button_input(&ctrl_start, DEV_CONTROLLER, 0, SDL_GAMEPAD_BUTTON_START);
    setup_chain_append(&cfg->input.game.players[0].keypad[KEY_START>>1], &ctrl_start);

    if (setup_mouse) {
        setup_make_mouse_input(&mouse_btm, PART_TYPE_BUTTON, DIR_PRESS);
        setup_chain_append(head, &mouse_btm);

        setup_make_mouse_input(&mouse_top, PART_TYPE_BUTTON, DIR_PRESS);
        mouse_top.part_num[0] = 3;
        setup_chain_append(&cfg->input.game.players[0].top_button, &mouse_top);

        setup_make_mouse_input(&mouse_start, PART_TYPE_BUTTON, DIR_PRESS);
        mouse_start.part_num[0] = 2;
        setup_chain_append(&cfg->input.game.players[0].keypad[KEY_START>>1],
                           &mouse_start);
    }

    memset(mask, 0, sizeof(mask));
    mask[0] = KCFG_INP_TYPE       | KCFG_INP_MOUSE_EN  | KCFG_INP_MOUSE_OPT  |
              KCFG_INP_JOY_SENS   |
              KCFG_INP_DPAD_AN_EN  |
              KCFG_INP_KEYBD_EN    | KCFG_INP_DPAD_EN   | KCFG_INP_JOY_EN     |
              KCFG_INP_JOY_LEFT    | KCFG_INP_JOY_RIGHT |
              KCFG_INP_JOY_UP      | KCFG_INP_JOY_DOWN  |
              KCFG_INP_JOY_TOP     | KCFG_INP_JOY_BTM  |
              KCFG_INP_JOY_KEY_STT;

    config_save_game_input(MACHINE_TYPE_5200, tbl, "this", mask, 0);
    setup_save_5200_joystick_pots(cfg, tbl, pot_profile);

    for (j = 0; j < MAX_CONTROLLER; ++j)
        cfg->input.game.players[j] = saved[j];
    cfg->general.machine = saved_machine;
    toml_free_node_tree(tbl);
}

#ifdef HAVE_ROM_DOWNLOAD
//#define ROMSET_DL_URL "https://archive.org/download/atari-5200-rom-collection/Atari%205200%20ROM%20Collection.zip"
#define ROMSET_DL_URL "https://archive.org/download/atari-5200-large-rom-collection/Atari_5200_Large_ROM_Collection.zip"

#ifdef HAVE_CURL
static int dl_xfer_cb(void *ud, curl_off_t dltotal, curl_off_t dlnow,
                      curl_off_t ult, curl_off_t uln)
{
    t_media_scan *scan = (t_media_scan *)ud;
    (void)ult; (void)uln;
    scan->progress = (dltotal > 0) ? (int)((dlnow * 99) / dltotal) + 1 : 1;
    return 0;
}

static size_t dl_write_cb(void *ptr, size_t sz, size_t nmemb, FILE *fp)
{
    return fwrite(ptr, sz, nmemb, fp);
}

/*
 * Per-transfer curl setup.  CURLSSLOPT_NATIVE_CA asks curl to validate against
 * the OS certificate store; it is a harmless no-op on backends without native
 * CA support.  (Windows builds download via WinHTTP, not curl -- see
 * dl_download_to_file -- so this path is Linux/macOS only.)
 */
static void dl_curl_apply_common(CURL *curl)
{
#ifdef CURLSSLOPT_NATIVE_CA
    curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NATIVE_CA);
#else
    (void)curl;
#endif
}
#endif /* HAVE_CURL */

static int dl_has_valid_ext(const char *filename)
{
    const char *dot = strrchr(filename, '.');
    if (!dot) return 0;
    return (strcasecmp(dot, ".a52") == 0 ||
            strcasecmp(dot, ".bin") == 0 ||
            strcasecmp(dot, ".car") == 0 ||
            strcasecmp(dot, ".dat") == 0 ||
            strcasecmp(dot, ".hyb") == 0 ||
            strcasecmp(dot, ".rom") == 0 ||
            strcasecmp(dot, ".zip") == 0);
}

static const char *dl_zip_basename(const char *path)
{
    const char *slash = strrchr(path, '/');
    const char *backslash = strrchr(path, '\\');

    if (backslash && (!slash || backslash > slash))
        slash = backslash;

    return slash ? slash + 1 : path;
}

static int dl_zip_entry_is_dir(const char *entry)
{
    size_t len = strlen(entry);

    if (!len) return 0;
    return entry[len - 1] == '/' || entry[len - 1] == '\\';
}

static int dl_zip_entry_is_safe(const char *entry)
{
    char component[KAT_PATH_MAX];
    int component_len = 0;
    const char *p;

    if (!entry || !entry[0]) return 0;
    if (entry[0] == '/' || entry[0] == '\\') return 0;
    if (((entry[0] >= 'A' && entry[0] <= 'Z') ||
         (entry[0] >= 'a' && entry[0] <= 'z')) && entry[1] == ':')
        return 0;

    for (p = entry; ; ++p) {
        if (*p == '/' || *p == '\\' || *p == '\0') {
            if (!component_len)
                return *p == '\0';

            component[component_len] = '\0';
            if (!strcmp(component, ".") || !strcmp(component, ".."))
                return 0;

            component_len = 0;
            if (*p == '\0')
                return 1;
            continue;
        }

        if (component_len >= KAT_PATH_MAX - 1)
            return 0;
        component[component_len++] = *p;
    }
}

static int dl_make_extract_path(const char *dest_dir, const char *entry,
                                char *out_path, size_t out_path_size)
{
    char rel_path[KAT_PATH_MAX];
    char sep = util_get_dir_seperator_character();
    char sep_str[2] = {0};
    int written;
    size_t dest_len;
    size_t i;

    for (i = 0; entry[i] && i < sizeof(rel_path) - 1; ++i)
        rel_path[i] = (entry[i] == '/' || entry[i] == '\\') ? sep : entry[i];

    if (entry[i])
        return 0;

    rel_path[i] = '\0';
    sep_str[0] = sep;
    dest_len = strlen(dest_dir);
    written = snprintf(out_path, out_path_size, "%s%s%s", dest_dir,
                       (dest_len && dest_dir[dest_len - 1] != '/' &&
                        dest_dir[dest_len - 1] != '\\') ? sep_str : "",
                       rel_path);
    return written > 0 && (size_t)written < out_path_size;
}

static void dl_create_parent_dirs(const char *path)
{
    char dir[KAT_PATH_MAX];
    char *slash;
    char *backslash;
    size_t len;

    len = strlen(path);
    if (!len || len >= sizeof(dir))
        return;

    strcpy(dir, path);
    slash = strrchr(dir, '/');
    backslash = strrchr(dir, '\\');
    if (backslash && (!slash || backslash > slash))
        slash = backslash;

    if (!slash)
        return;

    *slash = '\0';
    if (dir[0])
        SDL_CreateDirectory(dir);
}

/*
 * Extract ROM files from the downloaded zip.  Returns the number of files
 * written (>= 0) on success, or -1 if the archive could not be opened/read.
 * Per-entry write failures are logged but do not abort the whole extraction.
 * scan->progress is advanced (1..100) across the archive's entries so the UI
 * keeps moving during what can be a multi-minute extraction.
 */
static int dl_extract_zip(const char *zip_path, const char *dest_dir,
                          t_media_scan *scan)
{
    unzFile uf;
    unz_global_info ginfo;
    unz_file_info info;
    char entry[KAT_PATH_MAX];
    char out_path[KAT_PATH_MAX];
    char msg[KAT_PATH_MAX + 128];
    unsigned char buf[8192];
    int bytes;
    int extracted = 0;
    unsigned long total_entries = 0, entry_idx = 0;
    FILE *fp;
    const char *basename;

    uf = unzOpen(zip_path);
    if (!uf) {
        snprintf ( msg, sizeof(msg),
                   "ROM download: could not open zip archive %s", zip_path );
        logger_log_message ( LOG_ERROR, msg, "" );
        return -1;
    }

    if (unzGetGlobalInfo(uf, &ginfo) == UNZ_OK)
        total_entries = ginfo.number_entry;

    if (unzGoToFirstFile(uf) != UNZ_OK) {
        logger_log_message ( LOG_ERROR,
                             "ROM download: zip archive is empty or corrupt", "" );
        unzClose(uf);
        return -1;
    }

    do {
        /* Advance the bar per archive entry (every entry, not just ROMs we
           keep) so it reflects progress through the whole zip. */
        if (total_entries > 0)
            scan->progress = (int)((entry_idx++ * 99) / total_entries) + 1;

        unzGetCurrentFileInfo(uf, &info, entry, sizeof(entry), NULL, 0, NULL, 0);
        if (!dl_zip_entry_is_safe(entry)) continue;
        if (!dl_make_extract_path(dest_dir, entry, out_path, sizeof(out_path))) continue;

        if (dl_zip_entry_is_dir(entry)) {
            SDL_CreateDirectory(out_path);
            continue;
        }

        basename = dl_zip_basename(entry);
        if (!dl_has_valid_ext(basename)) continue;  /* skip non-ROM files */

        dl_create_parent_dirs(out_path);

        fp = fopen(out_path, "rb");                 /* skip if already exists */
        if (fp) { fclose(fp); continue; }

        if (unzOpenCurrentFile(uf) != UNZ_OK) {
            snprintf ( msg, sizeof(msg),
                       "ROM download: failed to read zip entry %s", entry );
            logger_log_message ( LOG_ERROR, msg, "" );
            continue;
        }

        fp = fopen(out_path, "wb");
        if (fp) {
            while ((bytes = unzReadCurrentFile(uf, buf, sizeof(buf))) > 0)
                fwrite(buf, 1, bytes, fp);
            fclose(fp);
            ++extracted;
        }
        else {
            snprintf ( msg, sizeof(msg),
                       "ROM download: could not write extracted file %s", out_path );
            logger_log_message ( LOG_ERROR, msg, "" );
        }
        unzCloseCurrentFile(uf);

    } while (unzGoToNextFile(uf) == UNZ_OK);

    unzClose(uf);
    return extracted;
}

/*
 * Download `url` to `out_path`, updating scan->progress (1..100) as bytes
 * arrive.  Returns 0 on success, non-zero on failure (the caller removes the
 * partial file).  Two backends: WinHTTP on Windows -- a system component that
 * validates against the OS certificate store, so the portable package ships no
 * libcurl/OpenSSL DLLs and no CA bundle -- and libcurl everywhere else.
 */
#if defined(_WIN32)

static int dl_download_to_file(const char *url, const char *out_path,
                               t_media_scan *scan)
{
    wchar_t   wurl[DL_URL_WMAX];
    wchar_t   whost[256];
    wchar_t   wpath[DL_URL_WMAX];
    wchar_t   wextra[DL_URL_WMAX];
    wchar_t   wresource[DL_URL_WMAX * 2];
    URL_COMPONENTS uc;
    HINTERNET hSession = NULL, hConnect = NULL, hRequest = NULL;
    FILE     *fp = NULL;
    DWORD     status = 0, status_sz = sizeof(status);
    DWORD     clen = 0, clen_sz = sizeof(clen);
    DWORD64   total = 0, received = 0;
    DWORD     flags;
    char      buf[16384];
    char      msg[KAT_PATH_MAX + 256];
    int       attempt;
    int       rc = 1;

    if (MultiByteToWideChar(CP_UTF8, 0, url, -1, wurl, DL_URL_WMAX) == 0) {
        logger_log_message(LOG_ERROR,
            "ROM download: could not convert URL to wide string", "");
        return 1;
    }

    /* Split into host + resource so we can drive WinHttpConnect/OpenRequest. */
    memset(&uc, 0, sizeof(uc));
    uc.dwStructSize  = sizeof(uc);
    uc.lpszHostName  = whost;  uc.dwHostNameLength  = ARRAYSIZE(whost);
    uc.lpszUrlPath   = wpath;  uc.dwUrlPathLength   = ARRAYSIZE(wpath);
    uc.lpszExtraInfo = wextra; uc.dwExtraInfoLength = ARRAYSIZE(wextra);
    if (!WinHttpCrackUrl(wurl, 0, 0, &uc)) {
        snprintf(msg, sizeof(msg),
                 "ROM download: malformed URL (WinHttpCrackUrl err %lu)",
                 (unsigned long)GetLastError());
        logger_log_message(LOG_ERROR, msg, "");
        return 1;
    }
    _snwprintf(wresource, ARRAYSIZE(wresource), L"%s%s", wpath, wextra);
    wresource[ARRAYSIZE(wresource) - 1] = L'\0';

    /* WinHTTP follows redirects by default (archive.org -> a storage node).
       Those nodes are load-balanced and a given one can return a transient 5xx
       while others serve fine, so retry the whole request: each fresh attempt
       re-hits the load balancer and usually lands on a healthy node.  Reuse the
       session/connection; only the request handle is one-shot per send. */
    hSession = WinHttpOpen(L"kat5200", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
                           WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
    if (hSession)
        hConnect = WinHttpConnect(hSession, whost, uc.nPort, 0);
    if (!hConnect) {
        snprintf(msg, sizeof(msg),
                 "ROM download: WinHTTP setup failed (err %lu)",
                 (unsigned long)GetLastError());
        logger_log_message(LOG_ERROR, msg, "");
        goto done;
    }
    flags = (uc.nScheme == INTERNET_SCHEME_HTTPS) ? WINHTTP_FLAG_SECURE : 0;

    for (attempt = 1; attempt <= DL_MAX_ATTEMPTS; ++attempt) {
        if (attempt > 1) {
            Sleep(1000 * (attempt - 1));   /* brief linear backoff */
            snprintf(msg, sizeof(msg),
                     "ROM download: retrying (attempt %d of %d)",
                     attempt, DL_MAX_ATTEMPTS);
            logger_log_message(LOG_INFO, msg, "");
        }

        if (hRequest) WinHttpCloseHandle(hRequest);
        hRequest = WinHttpOpenRequest(hConnect, L"GET", wresource, NULL,
                                      WINHTTP_NO_REFERER,
                                      WINHTTP_DEFAULT_ACCEPT_TYPES, flags);
        if (!hRequest) {
            snprintf(msg, sizeof(msg),
                     "ROM download: WinHTTP setup failed (err %lu)",
                     (unsigned long)GetLastError());
            logger_log_message(LOG_ERROR, msg, "");
            goto done;
        }

        if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
                                WINHTTP_NO_REQUEST_DATA, 0, 0, 0) ||
            !WinHttpReceiveResponse(hRequest, NULL)) {
            snprintf(msg, sizeof(msg),
                     "ROM download: transfer failed (WinHTTP err %lu)",
                     (unsigned long)GetLastError());
            logger_log_message(LOG_ERROR, msg, "");
            status = 0;
            continue;                       /* network blip -> retry */
        }

        status = 0; status_sz = sizeof(status);
        WinHttpQueryHeaders(hRequest,
            WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
            WINHTTP_HEADER_NAME_BY_INDEX, &status, &status_sz,
            WINHTTP_NO_HEADER_INDEX);

        if (status == 200)
            break;                          /* success */

        snprintf(msg, sizeof(msg),
                 "ROM download: server returned HTTP %lu%s",
                 (unsigned long)status,
                 (status >= 500) ? " (will retry)" : "");
        logger_log_message((status >= 500) ? LOG_INFO : LOG_ERROR, msg, "");

        if (status < 500)
            goto done;                      /* 4xx etc -- retrying won't help */
    }

    if (status != 200) {
        logger_log_message(LOG_ERROR,
            "ROM download: giving up after repeated server errors", "");
        goto done;
    }

    /* Content-Length drives the progress bar; absent/unknown -> indeterminate. */
    if (WinHttpQueryHeaders(hRequest,
            WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER,
            WINHTTP_HEADER_NAME_BY_INDEX, &clen, &clen_sz, WINHTTP_NO_HEADER_INDEX))
        total = clen;

    fp = fopen(out_path, "wb");
    if (!fp) {
        snprintf(msg, sizeof(msg),
                 "ROM download: could not open temp file for writing: %s",
                 out_path);
        logger_log_message(LOG_ERROR, msg, "");
        goto done;
    }

    for (;;) {
        DWORD avail = 0, got = 0, want;
        if (!WinHttpQueryDataAvailable(hRequest, &avail)) {
            snprintf(msg, sizeof(msg),
                     "ROM download: read failed (WinHTTP err %lu)",
                     (unsigned long)GetLastError());
            logger_log_message(LOG_ERROR, msg, "");
            goto done;
        }
        if (avail == 0) break;                  /* end of stream */
        want = (avail < sizeof(buf)) ? avail : (DWORD)sizeof(buf);
        if (!WinHttpReadData(hRequest, buf, want, &got)) {
            snprintf(msg, sizeof(msg),
                     "ROM download: read failed (WinHTTP err %lu)",
                     (unsigned long)GetLastError());
            logger_log_message(LOG_ERROR, msg, "");
            goto done;
        }
        if (got == 0) break;
        if (fwrite(buf, 1, got, fp) != got) {
            logger_log_message(LOG_ERROR,
                "ROM download: error writing to temp file", "");
            goto done;
        }
        received += got;
        scan->progress = (total > 0) ? (int)((received * 99) / total) + 1 : 1;
    }

    rc = 0;   /* success */

done:
    if (fp) fclose(fp);
    if (hRequest) WinHttpCloseHandle(hRequest);
    if (hConnect) WinHttpCloseHandle(hConnect);
    if (hSession) WinHttpCloseHandle(hSession);
    return rc;
}

#elif defined(__ANDROID__)

/*
 * Android backend: bridge to the Java KatDownloader.downloadToFile() over JNI,
 * which uses the platform HTTP stack (HttpURLConnection) and the system TLS
 * trust store -- no native curl/OpenSSL.  The Java side reports progress by
 * calling nativeProgress() below.
 *
 * Class + method are resolved in JNI_OnLoad (on the Java thread, where the app
 * class loader is in scope) and cached: FindClass() from the SDL-created
 * "RomDownload" worker thread would use the bootstrap loader and fail to find
 * an app class on many devices.
 */
static jclass        g_KatDownloader_cls;   /* global ref, set in JNI_OnLoad */
static jmethodID     g_downloadToFile_mid;
static t_media_scan *g_dl_scan;             /* progress target; UI serializes downloads */

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved)
{
    JNIEnv *env;
    jclass  cls;
    (void)reserved;

    if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK)
        return JNI_ERR;

    cls = (*env)->FindClass(env, "io/github/madjacket/kat5200/KatDownloader");
    if (cls) {
        g_KatDownloader_cls  = (*env)->NewGlobalRef(env, cls);
        g_downloadToFile_mid = (*env)->GetStaticMethodID(env, cls, "downloadToFile",
                                   "(Ljava/lang/String;Ljava/lang/String;)Z");
        (*env)->DeleteLocalRef(env, cls);
    }
    if ((*env)->ExceptionCheck(env))
        (*env)->ExceptionClear(env);

    return JNI_VERSION_1_6;
}

/* Called from Java as bytes arrive; clamps and stores into the active scan. */
JNIEXPORT void JNICALL
Java_io_github_madjacket_kat5200_KatDownloader_nativeProgress(JNIEnv *env, jclass cls,
                                                              jint percent)
{
    (void)env; (void)cls;
    if (g_dl_scan)
        g_dl_scan->progress = (percent < 1) ? 1 : (percent > 100 ? 100 : percent);
}

static int dl_download_to_file(const char *url, const char *out_path,
                               t_media_scan *scan)
{
    JNIEnv  *env;
    jstring  jurl = NULL, jout = NULL;
    jboolean ok = JNI_FALSE;

    if (!g_KatDownloader_cls || !g_downloadToFile_mid) {
        logger_log_message(LOG_ERROR,
            "ROM download: Java KatDownloader not available (JNI_OnLoad failed)", "");
        return 1;
    }

    env = (JNIEnv *)SDL_GetAndroidJNIEnv();
    if (!env) {
        logger_log_message(LOG_ERROR,
            "ROM download: no JNI environment on download thread", "");
        return 1;
    }

    g_dl_scan = scan;

    jurl = (*env)->NewStringUTF(env, url);
    jout = (*env)->NewStringUTF(env, out_path);
    if ((*env)->ExceptionCheck(env) || !jurl || !jout) {
        (*env)->ExceptionDescribe(env);
        (*env)->ExceptionClear(env);
        logger_log_message(LOG_ERROR,
            "ROM download: failed to build JNI string arguments", "");
        goto fail;
    }

    ok = (*env)->CallStaticBooleanMethod(env, g_KatDownloader_cls,
                                         g_downloadToFile_mid, jurl, jout);
    if ((*env)->ExceptionCheck(env)) {
        (*env)->ExceptionDescribe(env);
        (*env)->ExceptionClear(env);
        logger_log_message(LOG_ERROR,
            "ROM download: Java downloadToFile threw an exception", "");
        ok = JNI_FALSE;
    }

    if (jurl) (*env)->DeleteLocalRef(env, jurl);
    if (jout) (*env)->DeleteLocalRef(env, jout);
    g_dl_scan = NULL;
    return ok ? 0 : 1;

fail:
    if (jurl) (*env)->DeleteLocalRef(env, jurl);
    if (jout) (*env)->DeleteLocalRef(env, jout);
    g_dl_scan = NULL;
    return 1;
}

#elif defined(HAVE_CURL)

static int dl_download_to_file(const char *url, const char *out_path,
                               t_media_scan *scan)
{
    CURL    *curl;
    FILE    *fp;
    CURLcode res;
    long     http_code = 0;
    char     msg[KAT_PATH_MAX + 256];

    curl = curl_easy_init();
    if (!curl) {
        logger_log_message(LOG_ERROR,
                           "ROM download: curl_easy_init() failed", "");
        return 1;
    }

    fp = fopen(out_path, "wb");
    if (!fp) {
        snprintf(msg, sizeof(msg),
                 "ROM download: could not open temp file for writing: %s",
                 out_path);
        logger_log_message(LOG_ERROR, msg, "");
        curl_easy_cleanup(curl);
        return 1;
    }

    dl_curl_apply_common(curl);
    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, dl_write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, dl_xfer_cb);
    curl_easy_setopt(curl, CURLOPT_XFERINFODATA, scan);
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);

    res = curl_easy_perform(curl);
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
    curl_easy_cleanup(curl);
    fclose(fp);

    if (res != CURLE_OK) {
        snprintf(msg, sizeof(msg),
                 "ROM download: transfer failed (curl: %s), HTTP code %ld",
                 curl_easy_strerror(res), http_code);
        logger_log_message(LOG_ERROR, msg, "");
        return 1;
    }
    return 0;
}

#endif

static int setup_download_romset_thread(t_media_scan *scan)
{
    char tmp_zip[KAT_PATH_MAX];
    char msg[KAT_PATH_MAX + 256];
    int  extracted;
    t_config *cfg = config_get_global_cfg();
    const char *url = (cfg->general.download_url && cfg->general.download_url[0])
                      ? cfg->general.download_url
                      : ROMSET_DL_URL;

    snprintf ( msg, sizeof(msg), "ROM download: starting download from %s", url );
    logger_log_message ( LOG_INFO, msg, "" );

    SDL_CreateDirectory(scan->directory);
    /* ".part", not ".zip": if a concurrent scan happens to look at this folder
       it must not mistake the in-flight download for a ROM (see
       dl_has_valid_ext).  minizip opens by content, so the name is irrelevant
       to extraction. */
    snprintf(tmp_zip, sizeof(tmp_zip), "%s/_romset_dl.part", scan->directory);

    if (dl_download_to_file(url, tmp_zip, scan) != 0) {
        remove(tmp_zip);
        scan->progress = 0;
        guitop_show_toast_msg("ROM download failed. See log for details.");
        return 1;
    }

    logger_log_message ( LOG_INFO,
                         "ROM download: download succeeded, extracting...", "" );

    scan->progress_label = "Extracting";
    extracted = dl_extract_zip(tmp_zip, scan->directory, scan);
    remove(tmp_zip);

    if (extracted < 0) {
        logger_log_message ( LOG_ERROR,
                             "ROM download: extraction failed", "" );
        scan->progress = 0;
        guitop_show_toast_msg("ROM extraction failed. See log for details.");
        return 1;
    }

    snprintf ( msg, sizeof(msg),
               "ROM download: extraction succeeded, %d file(s) written to %s",
               extracted, scan->directory );
    logger_log_message ( LOG_INFO, msg, "" );

    scan->type = MACHINE_TYPE_5200;
    scan->flags = MEDIA_SCAN_BIOS | MEDIA_SCAN_MEDIA | MEDIA_SCAN_SUBS;
    scan->progress_label = NULL;   /* scan phase shows "Files Scanned" */
    media_if_scan_directory(scan);

    scan->progress = 0;
    return 0;
}

static void setup_save_dl_dir_result(struct modal_config *cfg)
{
    if (!cfg->result) return;
    util_alloc_string(&a5200_dl_params.directory, filebrowser_result());
}

static void setup_choose_dl_dir(struct option_widget *wgt, void *data)
{
    struct modal_config *cfg = guitop_get_modal_cfg();
    t_media_scan *scan = (t_media_scan *)data;
    (void)wgt;
    memset(cfg, 0, sizeof(struct modal_config));
    cfg->data = scan ? scan->directory : NULL;
    cfg->modal_type = MODAL_FB_DIR;
    cfg->modal_active = 2;
    cfg->id = 0;
    strcpy(cfg->title, "Choose Download Directory");
    cfg->post_show = setup_save_dl_dir_result;
    filebrowser_init(cfg);
}
#endif /* HAVE_ROM_DOWNLOAD */

void setup_scan_and_save(struct option_widget *wgt, void *data)
{
    int skip_5200_rom_scan = 0;

    setup_save_config(wgt, data);

#ifdef HAVE_ROM_DOWNLOAD
    /* The download thread scans its own directory after the unzip finishes.
       If the 5200 ROM scan targets that same folder, running it concurrently
       would scan a half-extracted directory, so let the download thread own
       it and skip the redundant standalone scan. */
    if (g_download_romset && a5200_dl_params.directory &&
        a5200_rom_scan_params.directory &&
        strcasecmp(a5200_rom_scan_params.directory,
                   a5200_dl_params.directory) == 0)
        skip_5200_rom_scan = 1;
#endif

    if (g_scan_5200_bios) {
        a5200_bios_scan_params.type = MACHINE_TYPE_5200;
        a5200_bios_scan_params.flags = MEDIA_SCAN_BIOS | MEDIA_SCAN_MEDIA | MEDIA_SCAN_SUBS;
        a5200_bios_scan_params.progress = 1;
        pc_start_thread(media_if_scan_directory, "A5200_BIOS_Scan", &a5200_bios_scan_params);
    }

    if (g_scan_5200_rom && !skip_5200_rom_scan) {
        a5200_rom_scan_params.type = MACHINE_TYPE_5200;
        a5200_rom_scan_params.flags = MEDIA_SCAN_MEDIA | MEDIA_SCAN_SUBS;
        a5200_rom_scan_params.progress = 1;
        pc_start_thread(media_if_scan_directory, "A5200_ROM_Scan", &a5200_rom_scan_params);
    }

    if (g_scan_a8_bios) {
        a8_bios_scan_params.type = MACHINE_TYPE_800;
        a8_bios_scan_params.flags = MEDIA_SCAN_BIOS | MEDIA_SCAN_MEDIA | MEDIA_SCAN_SUBS;
        a8_bios_scan_params.progress = 1;
        pc_start_thread(media_if_scan_directory, "A8_BIOS_Scan", &a8_bios_scan_params);
    }

    if (g_scan_a8_rom) {
        a8_rom_scan_params.type = MACHINE_TYPE_800;
        a8_rom_scan_params.flags = MEDIA_SCAN_BIOS | MEDIA_SCAN_MEDIA | MEDIA_SCAN_SUBS;
        a8_rom_scan_params.progress = 1;
        pc_start_thread(media_if_scan_directory, "A8_ROM_Scan", &a8_rom_scan_params);
    }

#ifdef HAVE_ROM_DOWNLOAD
    if (g_download_romset && !a5200_dl_params.progress) {
        a5200_dl_params.progress = 1;
        a5200_dl_params.progress_label = "Downloading";
        pc_start_thread(setup_download_romset_thread, "RomDownload", &a5200_dl_params);
    }
#endif
}

int menu_options_setup(struct option_widget *wid_ary)
{
    t_config *cfg = config_get_global_cfg();
    int i;

    if (!g_dir_set) {
        config_load_directories_by_machine(config_get_global_table(), MACHINE_TYPE_5200,
            &a5200_rom_scan_params.directory, &a5200_bios_scan_params.directory);
        config_load_directories_by_machine(config_get_global_table(), MACHINE_TYPE_800,
            &a8_rom_scan_params.directory, &a8_bios_scan_params.directory);

#ifdef HAVE_ROM_DOWNLOAD
        if (!a5200_dl_params.directory) {
            char tmp[KAT_PATH_MAX];
#ifdef __ANDROID__
            /* Land downloads in app-external storage so they are user-visible
             * in a file manager and sit beside sideloaded ROMs (no permission
             * needed): /sdcard/Android/data/<pkg>/files/Atari5200Rom. */
            char sd[KAT_PATH_MAX];
            pc_get_external_sdcard_path(sd);
            snprintf(tmp, sizeof(tmp), "%s%cAtari5200Rom", sd, util_get_dir_seperator_character() );
#else
            snprintf(tmp, sizeof(tmp), "%s%cAtari5200Rom", util_get_program_path(), util_get_dir_seperator_character() );
#endif
            util_alloc_string(&a5200_dl_params.directory, tmp);
        }
#endif

        g_dir_set = 1;
    }

    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 = KAT_MENU_OPTIONS_MISC;
        wid_ary[i].menu_next = 0;
        wid_ary[i].divider = 0;
        wid_ary[i].mask_value = 0;
        wid_ary[i].p_mask = &mask_setup;
        wid_ary[i].p_source_mask = NULL;
        wid_ary[i].data = NULL;
        wid_ary[i].get_string = NULL;
        wid_ary[i].run_task = NULL;
        wid_ary[i].accept_input = 1;
    }

    i = -1;

    /* Trackball Games */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Trackball Games");
    wid_ary[i].data = &g_enable_mouse_trackball_games;
    wid_ary[i].p_mask = NULL;

    if (g_enable_mouse_trackball_games) {
        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, "  Setup Mouse");
        wid_ary[i].data = &g_trackball_setup_mouse;
        wid_ary[i].p_mask = NULL;
    }

    if (g_enable_mouse_trackball_games && g_trackball_setup_mouse) {
        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Centipede");
        wid_ary[i].data = &g_mouse_trackball_centipede;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Missile Command");
        wid_ary[i].data = &g_mouse_trackball_missile_cmd;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Super Breakout");
        wid_ary[i].data = &g_mouse_trackball_super_breakout;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Bug Hunt Trackball");
        wid_ary[i].data = &g_mouse_trackball_bug_hunt;
        wid_ary[i].p_mask = NULL;
    }

    /* Paddle/Cursor Games */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Paddle/Cursor Games");
    wid_ary[i].data = &g_enable_mouse_joystick_games;
    wid_ary[i].p_mask = NULL;

    if (g_enable_mouse_joystick_games) {
        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, "  Setup Mouse");
        wid_ary[i].data = &g_pcgames_setup_mouse;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, "  Setup Digital To Analog");
        wid_ary[i].data = &g_pcgames_digital_analog;
        wid_ary[i].p_mask = NULL;
    }

    if (g_enable_mouse_joystick_games &&
            (g_pcgames_setup_mouse || g_pcgames_digital_analog)) {
        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Star Wars Arcade");
        wid_ary[i].data = &g_mouse_joystick_star_wars_arcade;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Gorf");
        wid_ary[i].data = &g_mouse_joystick_gorf;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Castle Crises");
        wid_ary[i].data = &g_mouse_joystick_castle_crises;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Kaboom");
        wid_ary[i].data = &g_mouse_joystick_kaboom;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Bug Hunt Joystick");
        wid_ary[i].data = &g_mouse_joystick_bug_hunt;
        wid_ary[i].p_mask = NULL;
    }

    /* Setup 2 Stick Games */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Setup 2 Stick Games");
    wid_ary[i].data = &g_setup_2stick_games;
    wid_ary[i].p_mask = NULL;

    /* 2-stick info label */
    //wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    //strcpy(wid_ary[i].label, "(Use Right Thumbstick)");
    //wid_ary[i].accept_input = 0;

    if (g_setup_2stick_games) {
        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Space Dungeon");
        wid_ary[i].data = &g_2stick_space_dungeon;
        wid_ary[i].p_mask = NULL;

        wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
        strcpy(wid_ary[i].label, SETUP_GAME_PREFIX "Robotron 2084");
        wid_ary[i].data = &g_2stick_robotron;
        wid_ary[i].p_mask = NULL;
    }

#ifdef HAVE_ROM_DOWNLOAD
    /* Download 5200 ROM Set */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Download 5200 ROM Set");
    wid_ary[i].data = &g_download_romset;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].divider = 1;

    if (g_download_romset) {
        wid_ary[++i].widget_type = WIDGET_TYPE_STRING_SCAN;
        strcpy(wid_ary[i].label, "Download Directory");
        wid_ary[i].data = &a5200_dl_params;
        wid_ary[i].mask_value = 0;
        wid_ary[i].get_string = NULL;
        wid_ary[i].run_task = setup_choose_dl_dir;
    }
#endif

    /* 5200 BIOS toggle + directory */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Scan for 5200 BIOS");
    wid_ary[i].divider = 1;
    wid_ary[i].data = &g_scan_5200_bios;
    wid_ary[i].p_mask = NULL;

    wid_ary[++i].widget_type = WIDGET_TYPE_STRING_SCAN;
    strcpy(wid_ary[i].label, "5200 BIOS Directory");
    wid_ary[i].data = &a5200_bios_scan_params;
    wid_ary[i].mask_value = KCFG_MACHINE_BIOS_DIR;
    wid_ary[i].get_string = NULL;
    wid_ary[i].run_task = setup_choose_5200_bit_dir;

    /* 5200 ROM toggle + directory */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Scan for 5200 ROMs");
    wid_ary[i].data = &g_scan_5200_rom;
    wid_ary[i].p_mask = NULL;

    wid_ary[++i].widget_type = WIDGET_TYPE_STRING_SCAN;
    strcpy(wid_ary[i].label, "5200 ROM Directory");
    wid_ary[i].data = &a5200_rom_scan_params;
    wid_ary[i].mask_value = KCFG_MACHINE_ROM_DIR;
    wid_ary[i].get_string = NULL;
    wid_ary[i].run_task = setup_choose_5200_bit_dir;

    /* 8-bit BIOS toggle + directory */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Scan for 8-bit BIOS");
    wid_ary[i].data = &g_scan_a8_bios;
    wid_ary[i].p_mask = NULL;

    wid_ary[++i].widget_type = WIDGET_TYPE_STRING_SCAN;
    strcpy(wid_ary[i].label, "8-bit BIOS Directory");
    wid_ary[i].data = &a8_bios_scan_params;
    wid_ary[i].mask_value = KCFG_MACHINE_BIOS_DIR;
    wid_ary[i].get_string = NULL;
    wid_ary[i].run_task = setup_choose_8_bit_dir;

    /* 8-bit ROM toggle + directory */
    wid_ary[++i].widget_type = WIDGET_TYPE_TOGGLE;
    strcpy(wid_ary[i].label, "Scan for 8-bit ROMs");
    wid_ary[i].data = &g_scan_a8_rom;
    wid_ary[i].p_mask = NULL;

    wid_ary[++i].widget_type = WIDGET_TYPE_STRING_SCAN;
    strcpy(wid_ary[i].label, "8-bit ROM Directory");
    wid_ary[i].data = &a8_rom_scan_params;
    wid_ary[i].mask_value = KCFG_MACHINE_ROM_DIR;
    wid_ary[i].get_string = NULL;
    wid_ary[i].run_task = setup_choose_8_bit_dir;

    /* Scan & Save - divider before this item */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
#ifdef HAVE_ROM_DOWNLOAD
    if (g_download_romset)
        strcpy(wid_ary[i].label, "\xef\x83\x87  Download, Scan & Save");
    else
        strcpy(wid_ary[i].label, "\xef\x83\x87  Scan & Save");
#else
    strcpy(wid_ary[i].label, "\xef\x83\x87  Scan & Save");
#endif
    wid_ary[i].data = NULL;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].get_string = NULL;
    wid_ary[i].run_task = setup_scan_and_save;
    wid_ary[i].divider = 1;

    /* Save only */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy(wid_ary[i].label, "\xef\x83\x87  Save only");
    wid_ary[i].data = NULL;
    wid_ary[i].p_mask = NULL;
    wid_ary[i].get_string = NULL;
    wid_ary[i].run_task = setup_save_config;

    return i + 1;
}
