/******************************************************************************
*
* Copyright (C) 2024  Brian Berlin
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************
*
* FILENAME: launcher.c
*
* DESCRIPTION:  This implements the ROM launcher for browsing and selecting ROMs
*
* AUTHORS:  bberlin
*
* CHANGE LOG
*
*  VER     DATE     CHANGED BY   DESCRIPTION OF CHANGE
* ------  --------  ----------   ------------------------------------
* 0.9.1   01/01/24  bberlin      Creation
******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <dirent.h>
#include <sys/stat.h>

#include "launcher.h"
#include "menu.h"
#include "theme.h"
#include "../interface/logger.h"
#include "input.h"
#include "modal.h"
#include "message.h"
#include "filebrowser.h"
#include "guitop.h"
#include "gui.h"
#include "../interface/katconfig.h"
#include "../interface/media_if.h"
#include "../interface/util.h"
#include "../interface/sdl_if.h"
#include "../core/cart.h"
#include "../core/console.h"

extern struct theme g_theme;
extern struct modal_config g_modal_cfg;

/* Launcher state */
static struct {
    int active;                    /* Is launcher active */
    int current_section;           /* Current button selection (0-3) */
    int selected_rom_index;        /* Currently selected ROM in list */
    unsigned int selected_crc;     /* CRC of selected ROM */
    t_toml_node *rom_profile;      /* Parsed profile for selected ROM */
    int rom_profile_load_deferred; /* Profile load postponed during scrollbar drag */

    /* ROM list data */
    t_rom_entry *rom_list;         /* Array of ROM entries */
    int rom_count;                 /* Number of ROMs in current list */

    /* CSV file list for filtering */
    char **csv_files;              /* Array of CSV filenames (without .csv) */
    int csv_file_count;            /* Number of CSV files */
    int current_csv_index;         /* Currently selected CSV (0=All, 1=Favorites, 2+=csv files) */

    /* ROM info from profile */
    char rom_name[128];
    char rom_filename[256];
    int rom_size;
    char rom_mapping[32];

    /* Options state */
    int attach_to;                 /* ATTACH_TO_* value */
    int mapping_index;             /* Current mapping selection */
} g_launcher = {0};

/* Cross-modal state for "Add from File" flow */
static char      s_add_file_path[1024];
static unsigned int s_add_file_crc;
static int       s_add_file_size;
static char      s_add_file_md5[33];
static char      s_add_file_sha1[41];

/* Forward declarations */
static void launcher_scan_csv_files(void);
static void launcher_load_rom_list(const char *csv_filename);
static void launcher_commit_deferred_profile(void);
static void launcher_select_rom(int index);
static void launcher_select_rom_base(int index);
static void launcher_select_rom_deferred(int index);
static void launcher_free_rom_list(void);
static void launcher_free_csv_files(void);
static char *launcher_get_attach_to_string(struct option_widget *wgt, char *buf);
static char *launcher_get_mapping_string(struct option_widget *wgt, char *buf);
static char *launcher_get_view_string(struct option_widget *wgt, char *buf);
static void launcher_action_launch(struct option_widget *wgt, void *data);
static void launcher_action_launch_no_reset(struct option_widget *wgt, void *data);
static void launcher_action_attach(struct option_widget *wgt, void *data);
static void launcher_action_save(struct option_widget *wgt, void *data);
static void launcher_action_clear_custom(struct option_widget *wgt, void *data);
static void launcher_action_add_favorite(struct option_widget *wgt, void *data);
static void launcher_action_clear_filename(struct option_widget *wgt, void *data);
static void launcher_action_edit_name(struct option_widget *wgt, void *data);
static void launcher_action_set_file(struct option_widget *wgt, void *data);
static void launcher_action_add_from_file(struct option_widget *wgt, void *data);
static void launcher_edit_name_result(struct modal_config *cfg);
static void launcher_file_result(struct modal_config *cfg);
static void launcher_add_from_file_name_result(struct modal_config *cfg);
static void launcher_add_from_file_fb_result(struct modal_config *cfg);
static void launcher_update_favorites_title(unsigned long crc, const char *new_title);
static char *launcher_get_favorites_path(void);
static int launcher_is_favorite(unsigned int crc);
static void launcher_action_remove_favorite(struct option_widget *wgt, void *data);
static void launcher_action_delete(struct option_widget *wgt, void *data);
static void launcher_delete_confirmed(struct modal_config *cfg);
static int launcher_add_rom_info_widgets(struct option_widget *wid_ary, int num_items);

/******************************************************************************
**  Function   :  launcher_launch_rom
**
**  Objective  :  Launch the currently selected ROM via gui_launch. Shows an
**                error message if no filename is associated with the ROM.
**
**  Parameters :  NONE
**
**  return     :  NONE
******************************************************************************/
void launcher_launch_rom(void)
{
    launcher_commit_deferred_profile();

    if (!g_launcher.rom_filename[0]) {
        struct modal_config *cfg = guitop_get_modal_cfg();
        memset(cfg, 0, sizeof(struct modal_config));
        cfg->modal_type = MODAL_MESSAGE;
        cfg->modal_active = 1;
        cfg->msg_type = KAT_MSG_TYPE_WARN;
        strcpy(cfg->title, "Launch ROM");
        strcpy(cfg->cancel_btn_label, "OK");
        snprintf(cfg->msg, sizeof(cfg->msg), "No filename set for this ROM");
        message_init(cfg);
        return;
    }

    util_alloc_string(&config_get_global_cfg()->general.rom, g_launcher.rom_filename);

    /* The selected profile is only needed while the launcher is visible. */
    if (g_launcher.rom_profile) {
        toml_free_node_tree(g_launcher.rom_profile);
        g_launcher.rom_profile = NULL;
    }

    gui_launch(1);
}

/******************************************************************************
**  Function   :  launcher_init
**
**  Objective  :  Initialize launcher state when entering launcher
**
**  Parameters :  NONE
**
**  return     :  NONE
******************************************************************************/
void launcher_init(void)
{
    memset(&g_launcher, 0, sizeof(g_launcher));
    g_launcher.active = 1;
    g_launcher.current_section = LAUNCHER_SECTION_ROM_LIST;
    g_launcher.selected_rom_index = -1;
    g_launcher.attach_to = ATTACH_TO_INDEX_CART;

    /* Scan for available CSV files */
    launcher_scan_csv_files();

    /* Load the default ROM list (All) */
    launcher_load_rom_list("list.csv");

    /* Select the currently loaded ROM if it's in the list, otherwise select first */
    if (g_launcher.rom_count > 0) {
        int start_index = 0;
        t_media *current = media_if_get_ptr();
        if (current && current->crc) {
            for (int i = 0; i < g_launcher.rom_count; i++) {
                if (g_launcher.rom_list[i].crc == (unsigned int)current->crc) {
                    start_index = i;
                    break;
                }
            }
        }
        launcher_select_rom(start_index);
    }
}

/******************************************************************************
**  Function   :  launcher_cleanup
**
**  Objective  :  Free allocated memory when leaving launcher
**
**  Parameters :  NONE
**
**  return     :  NONE
******************************************************************************/
void launcher_cleanup(void)
{
    launcher_free_rom_list();
    launcher_free_csv_files();

    if (g_launcher.rom_profile) {
        toml_free_node_tree(g_launcher.rom_profile);
        g_launcher.rom_profile = NULL;
    }

    g_launcher.active = 0;
}

/******************************************************************************
**  Function   :  launcher_free_rom_list
**
**  Objective  :  Free the ROM list array
**
**  Parameters :  NONE
**
**  return     :  NONE
******************************************************************************/
static void launcher_free_rom_list(void)
{
    if (g_launcher.rom_list) {
        free(g_launcher.rom_list);
        g_launcher.rom_list = NULL;
    }
    g_launcher.rom_count = 0;
    g_launcher.rom_profile_load_deferred = 0;
}

/******************************************************************************
**  Function   :  launcher_free_csv_files
**
**  Objective  :  Free the CSV files array
**
**  Parameters :  NONE
**
**  return     :  NONE
******************************************************************************/
static void launcher_free_csv_files(void)
{
    if (g_launcher.csv_files) {
        for (int i = 0; i < g_launcher.csv_file_count; i++) {
            free(g_launcher.csv_files[i]);
        }
        free(g_launcher.csv_files);
        g_launcher.csv_files = NULL;
    }
    g_launcher.csv_file_count = 0;
}

/******************************************************************************
**  Function   :  launcher_scan_csv_files
**
**  Objective  :  Scan profile directory for CSV files
**
**  Parameters :  NONE
**
**  return     :  NONE
******************************************************************************/
static void launcher_scan_csv_files(void)
{
    char *profiles_dir;
    char *full_dir;
    DIR *dir;
    struct dirent *entry;
    int count = 0;
    int capacity = 16;

    launcher_free_csv_files();

    /* Start with All and Favorites as default options */
    g_launcher.csv_files = malloc(capacity * sizeof(char *));
    if (!g_launcher.csv_files)
        return;

    g_launcher.csv_files[count++] = strdup("All");
    g_launcher.csv_files[count++] = strdup("Favorites");

    /* Get profiles directory for current machine */
    profiles_dir = config_get_profiles_dir(config_get_global_cfg()->general.machine);
    if (!profiles_dir) {
        g_launcher.csv_file_count = count;
        return;
    }

    full_dir = util_create_program_path_alloc(profiles_dir);
    free(profiles_dir);

    if (!full_dir) {
        g_launcher.csv_file_count = count;
        return;
    }

    dir = opendir(full_dir);
    if (!dir) {
        free(full_dir);
        g_launcher.csv_file_count = count;
        return;
    }

    while ((entry = readdir(dir)) != NULL) {
        size_t len = strlen(entry->d_name);

        /* Check for .csv extension */
        if (len > 4 && strcmp(entry->d_name + len - 4, ".csv") == 0) {
            /* Skip list.csv and favorites.csv as they're already included */
            if (strcmp(entry->d_name, "list.csv") == 0 ||
                strcmp(entry->d_name, "favorites.csv") == 0) {
                continue;
            }

            /* Grow array if needed */
            if (count >= capacity) {
                capacity *= 2;
                char **new_array = realloc(g_launcher.csv_files, capacity * sizeof(char *));
                if (!new_array)
                    break;
                g_launcher.csv_files = new_array;
            }

            /* Store filename without .csv extension */
            char *name = malloc(len - 3);
            if (name) {
                strncpy(name, entry->d_name, len - 4);
                name[len - 4] = '\0';
                g_launcher.csv_files[count++] = name;
            }
        }
    }

    closedir(dir);
    free(full_dir);

    g_launcher.csv_file_count = count;
}

static int launcher_compare_titles(const void *a, const void *b)
{
    return strcasecmp(((const t_rom_entry *)a)->title,
                      ((const t_rom_entry *)b)->title);
}

static char *launcher_trim_csv_field(char *field)
{
    char *end;

    while (isspace((unsigned char)*field))
        field++;

    end = field + strlen(field);
    while (end > field && isspace((unsigned char)end[-1]))
        *--end = '\0';

    return field;
}

static int launcher_parse_csv_entry(char *line, char *title, size_t title_size,
        unsigned int *crc)
{
    char *comma;
    char *title_start;
    char *crc_str;
    size_t len;

    if (!title_size)
        return 0;

    /* CRC is always the last field; titles may contain commas. */
    comma = strrchr(line, ',');
    if (!comma)
        return 0;

    *comma = '\0';
    title_start = launcher_trim_csv_field(line);
    crc_str = launcher_trim_csv_field(comma + 1);

    len = strlen(title_start);
    if (len >= 2 && title_start[0] == '"' && title_start[len - 1] == '"') {
        title_start[len - 1] = '\0';
        title_start++;
    }

    strncpy(title, title_start, title_size - 1);
    title[title_size - 1] = '\0';
    *crc = (unsigned int)strtoul(crc_str, NULL, 16);

    return 1;
}

static void launcher_write_csv_entry(FILE *fp, const char *title, unsigned int crc)
{
    fprintf(fp, "\"%s\", 0x%08x\n", title ? title : "", crc);
}

/******************************************************************************
**  Function   :  launcher_load_rom_list
**
**  Objective  :  Load ROM list from a CSV file
**
**  Parameters :  csv_filename - name of CSV file to load
**
**  return     :  NONE
******************************************************************************/
static void launcher_load_rom_list(const char *csv_filename)
{
    char *profiles_dir;
    char *full_path;
    char *temp;
    FILE *fp;
    char line[256];
    int capacity = 256;
    int count = 0;

    launcher_free_rom_list();

    /* Get profiles directory */
    profiles_dir = config_get_profiles_dir(config_get_global_cfg()->general.machine);
    if (!profiles_dir)
        return;

    temp = util_make_path_alloc(profiles_dir, csv_filename);
    free(profiles_dir);

    if (!temp)
        return;

    full_path = util_create_program_path_alloc(temp);
    free(temp);

    if (!full_path)
        return;

    fp = fopen(full_path, "r");
    free(full_path);

    if (!fp)
        return;

    /* Allocate initial ROM list */
    g_launcher.rom_list = malloc(capacity * sizeof(t_rom_entry));
    if (!g_launcher.rom_list) {
        fclose(fp);
        return;
    }

    /* Parse CSV: title,crc per line */
    while (fgets(line, sizeof(line), fp)) {
        char title[sizeof(g_launcher.rom_list[0].title)];
        unsigned int crc;

        /* Skip empty lines and comments */
        if (line[0] == '\n' || line[0] == '#' || line[0] == '\0')
            continue;

        /* Remove newline */
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n')
            line[len - 1] = '\0';

        if (!launcher_parse_csv_entry(line, title, sizeof(title), &crc))
            continue;

        /* Grow array if needed */
        if (count >= capacity) {
            capacity *= 2;
            t_rom_entry *new_list = realloc(g_launcher.rom_list, capacity * sizeof(t_rom_entry));
            if (!new_list)
                break;
            g_launcher.rom_list = new_list;
        }

        /* Store entry */
        strncpy(g_launcher.rom_list[count].title, title, sizeof(g_launcher.rom_list[count].title) - 1);
        g_launcher.rom_list[count].title[sizeof(g_launcher.rom_list[count].title) - 1] = '\0';
        g_launcher.rom_list[count].crc = crc;
        count++;
    }

    fclose(fp);
    g_launcher.rom_count = count;

    /* Sort alphabetically so binary search can be used for type-ahead */
    if (count > 1)
        qsort(g_launcher.rom_list, count, sizeof(t_rom_entry), launcher_compare_titles);
}

static void launcher_free_selected_profile(void)
{
    if (g_launcher.rom_profile) {
        toml_free_node_tree(g_launcher.rom_profile);
        g_launcher.rom_profile = NULL;
    }
}

static void launcher_select_rom_base(int index)
{
    if (index < 0 || index >= g_launcher.rom_count)
        return;

    g_launcher.selected_rom_index = index;
    g_launcher.selected_crc = g_launcher.rom_list[index].crc;
    g_launcher.rom_profile_load_deferred = 1;

    launcher_free_selected_profile();

    /* Reset info fields */
    strncpy(g_launcher.rom_name, g_launcher.rom_list[index].title,
            sizeof(g_launcher.rom_name) - 1);
    g_launcher.rom_name[sizeof(g_launcher.rom_name) - 1] = '\0';
    g_launcher.rom_filename[0] = '\0';
    g_launcher.rom_size = 0;
    strcpy(g_launcher.rom_mapping, "Auto");
    g_launcher.mapping_index = CART_MIN;
}

static void launcher_load_selected_profile(void)
{
    char *profile_path;

    if (g_launcher.selected_rom_index < 0 ||
        g_launcher.selected_rom_index >= g_launcher.rom_count) {
        g_launcher.rom_profile_load_deferred = 0;
        return;
    }

    /* Try to load profile for this CRC */
    launcher_free_selected_profile();
    profile_path = media_if_get_profile_from_crc(g_launcher.selected_crc);
    if (profile_path) {
        g_launcher.rom_profile = toml_parse_file(profile_path);
        free(profile_path);

        if (g_launcher.rom_profile) {
            char *value;

            /* Extract info from profile */
            value = toml_get_key_value(g_launcher.rom_profile, "title");
            if (value) {
                strncpy(g_launcher.rom_name, value, sizeof(g_launcher.rom_name) - 1);
                g_launcher.rom_name[sizeof(g_launcher.rom_name) - 1] = '\0';
            }

            value = toml_get_key_value(g_launcher.rom_profile, "file");
            if (value && strlen(value)) {
                strncpy(g_launcher.rom_filename, value, sizeof(g_launcher.rom_filename) - 1);
                g_launcher.rom_filename[sizeof(g_launcher.rom_filename) - 1] = '\0';
            }

            value = toml_get_key_value(g_launcher.rom_profile, "image_size");
            if (value) {
                g_launcher.rom_size = atoi(value);
            }

            value = toml_get_key_value(g_launcher.rom_profile, "mapping");
            if (value && strlen(value)) {
                g_launcher.mapping_index = media_if_get_mapping_from_string(value);
                cart_get_mapping_string(g_launcher.mapping_index, g_launcher.rom_mapping);
            }
        }
    }

    g_launcher.rom_profile_load_deferred = 0;
}

/******************************************************************************
**  Function   :  launcher_select_rom
**
**  Objective  :  Called when a ROM is selected in the list
**
**  Parameters :  index - index of selected ROM
**
**  return     :  NONE
******************************************************************************/
static void launcher_select_rom(int index)
{
    if (index < 0 || index >= g_launcher.rom_count)
        return;

    launcher_select_rom_base(index);
    launcher_load_selected_profile();
}

static void launcher_select_rom_deferred(int index)
{
    if (index < 0 || index >= g_launcher.rom_count)
        return;

    launcher_select_rom_base(index);
}

static void launcher_commit_deferred_profile(void)
{
    if (g_launcher.rom_profile_load_deferred)
        launcher_load_selected_profile();
}

/******************************************************************************
**  Function   :  launcher_is_active
**
**  Objective  :  Check if launcher is currently active
**
**  Parameters :  NONE
**
**  return     :  1 if active, 0 otherwise
******************************************************************************/
int launcher_is_active(void)
{
    return g_launcher.active;
}

/******************************************************************************
**  Function   :  launcher_get_section
**
**  Objective  :  Get the current launcher section
**
**  Parameters :  NONE
**
**  return     :  Current section index
******************************************************************************/
int launcher_get_section(void)
{
    return g_launcher.current_section;
}

/******************************************************************************
**  Function   :  launcher_menu_row
**
**  Objective  :  Render the launcher button row
**
**  Parameters :  ctx - Nuklear context
**
**  return     :  1 if section changed, 0 otherwise
******************************************************************************/
int launcher_menu_row(struct nk_context *ctx)
{
    int num_buttons = 4;
    static const char *button_labels[4] = {
        "\xef\x80\xba ROM List",      /* fa-list */
        "\xef\x83\xa7 ROM Actions",   /* fa-bolt */
        "\xef\x80\x93 ROM Options",   /* fa-gear */
        "\xef\x81\xae View Options"   /* fa-eye */
    };
    int select_this = 0;
    int changed = 0;
    int focused_button = g_launcher.current_section;

    if (!g_modal_cfg.modal_active)
        changed = input_check_horizontal_navigation(ctx, &focused_button, num_buttons);

    /* Calculate layout */
    int button_width = 150;  /* was 120 — extra 30px for icon glyph + space */
    int spacing_width = 30;
    int total_width = (num_buttons * button_width) + ((num_buttons - 1) * spacing_width);

    struct nk_rect window_bounds = nk_window_get_bounds(ctx);
    int window_width = (int)window_bounds.w;
    int start_x = window_width / 30;

    ctx->style.window.fixed_background.data.color = g_theme.menu_background;

    nk_layout_row_dynamic(ctx, 83, 1);

    nk_style_push_vec2(ctx, &ctx->style.window.group_padding, nk_vec2(0, 0));
    nk_style_push_vec2(ctx, &ctx->style.window.spacing, nk_vec2(0, 0));
    nk_style_push_float(ctx, &ctx->style.window.group_border, 0.0f);
    nk_style_push_style_item(ctx, &ctx->style.button.active, nk_style_item_color(g_theme.menu_background));
    nk_style_push_style_item(ctx, &ctx->style.button.hover, nk_style_item_color(g_theme.menu_background));
    nk_style_push_style_item(ctx, &ctx->style.button.normal, nk_style_item_color(g_theme.menu_background));
    nk_style_push_color(ctx, &ctx->style.button.text_hover, g_theme.menu_unselected);
    nk_style_push_color(ctx, &ctx->style.button.text_normal, g_theme.menu_unselected);
    nk_style_push_color(ctx, &ctx->style.button.text_active, g_theme.menu_unselected);

    if (nk_group_begin(ctx, "launcher_menu_row", NK_WINDOW_NO_SCROLLBAR)) {

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

        if (g_theme.use_compact_menu) {
            /* Compact mode: back chevron + gap + hamburger button + gap + current tab label */
            nk_layout_row_begin(ctx, NK_STATIC, 40, 6);

            nk_layout_row_push(ctx, start_x);
            nk_spacing(ctx, 1);

            nk_layout_row_push(ctx, 40);
            if (nk_button_label(ctx, "\xef\x81\x93") && !g_modal_cfg.modal_active)
                guitop_esc();

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

            nk_layout_row_push(ctx, 50);
            if (nk_button_label(ctx, "\xef\x83\x89") && !g_modal_cfg.modal_active)  /* fa-bars */
                guitop_hamburger_set(!guitop_hamburger_open());

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

            nk_layout_row_push(ctx, window_width - start_x - 40 - 10 - 50 - 10 - 8);
            theme_push_selected_button_color(ctx);
            nk_label(ctx, button_labels[focused_button], NK_TEXT_LEFT);
            theme_pop_selected_button_color(ctx);

            nk_layout_row_end(ctx);

            /* Dropdown popup — also entered when close_pending to call nk_popup_close cleanly */
            if (guitop_hamburger_open() || guitop_hamburger_close_pending()) {
                struct nk_rect popup_rect = nk_rect(start_x + 42, 55, 240, num_buttons * 45 + 10);
                if (nk_popup_begin(ctx, NK_POPUP_STATIC, "tab_select",
                                   NK_WINDOW_NO_SCROLLBAR, popup_rect)) {
                    if (guitop_hamburger_close_pending()) {
                        guitop_hamburger_clear_pending();
                        nk_popup_close(ctx);
                    } else {
                        nk_layout_row_dynamic(ctx, 40, 1);
                        for (int i = 0; i < num_buttons; i++) {
                            if (i == focused_button)
                                theme_push_selected_button_color(ctx);
                            if (nk_button_label(ctx, button_labels[i]) && !g_modal_cfg.modal_active) {
                                select_this = 1;
                                changed = 1;
                                guitop_hamburger_set(0);
                                guitop_consume_navigation_input(ctx);
                                nk_popup_close(ctx);
                            }
                            if (i == focused_button)
                                theme_pop_selected_button_color(ctx);
                            if (select_this) {
                                focused_button = i;
                                select_this = 0;
                            }
                        }
                    }
                    nk_popup_end(ctx);
                } else {
                    guitop_hamburger_set(0);
                    guitop_hamburger_clear_pending();
                }
            }
        } else {
            /* Full tab bar mode */
            nk_layout_row_begin(ctx, NK_STATIC, 40, num_buttons * 2 + 2);

            nk_layout_row_push(ctx, start_x);
            nk_spacing(ctx, 1);

            nk_layout_row_push(ctx, 40);
            if (nk_button_label(ctx, "\xef\x81\x93") && !g_modal_cfg.modal_active)
                guitop_esc();
            nk_layout_row_push(ctx, 20);
            nk_spacing(ctx, 1);

            struct nk_command_buffer *canvas = nk_window_get_canvas(ctx);

            for (int i = 0; i < num_buttons; i++) {
                nk_layout_row_push(ctx, button_width);

                struct nk_rect bounds = nk_widget_bounds(ctx);

                if (i == focused_button)
                    theme_push_selected_button_color(ctx);

                if (nk_button_label(ctx, button_labels[i]) && !g_modal_cfg.modal_active) {
                    select_this = 1;
                    changed = 1;
                }

                if (i == focused_button) {
                    struct nk_rect indicator = nk_rect(
                        bounds.x + 8, bounds.y + bounds.h - 3,
                        bounds.w - 16, 3);
                    nk_fill_rect(canvas, indicator, 1.5f, g_theme.tab_indicator);
                    theme_pop_selected_button_color(ctx);
                }

                if ((i + 1) != num_buttons) {
                    nk_layout_row_push(ctx, spacing_width);
                    nk_spacing(ctx, 1);
                }

                if (select_this) {
                    focused_button = i;
                    select_this = 0;
                }
            }
            nk_layout_row_end(ctx);
        }

        nk_layout_row_dynamic(ctx, 10, 1);
        nk_spacing(ctx, 1);
        guitop_draw_colorband_divider(ctx, g_theme.use_compact_menu);

        nk_group_end(ctx);
    }

    nk_style_pop_float(ctx);
    nk_style_pop_vec2(ctx);
    nk_style_pop_vec2(ctx);
    nk_style_pop_style_item(ctx);
    nk_style_pop_style_item(ctx);
    nk_style_pop_style_item(ctx);
    nk_style_pop_color(ctx);
    nk_style_pop_color(ctx);
    nk_style_pop_color(ctx);

    ctx->style.window.fixed_background.data.color = g_theme.background;

    if (changed) {
        g_launcher.current_section = focused_button;
    }

    return changed;
}

static float launcher_scrollbar_thumb_center_y(struct nk_context *ctx,
        struct nk_rect list_bounds, nk_uint scroll_y, int actual_height,
        int row_count)
{
    const struct nk_style_scrollbar *style = &ctx->style.scrollv;
    float scroll_track_y = list_bounds.y;
    float scroll_track_h = list_bounds.h;
    float target = (float)(actual_height * (row_count > 0 ? row_count : 1));
    float scroll_bar_w = ctx->style.window.scrollbar_size.x;

    if (style->show_buttons) {
        scroll_track_y += scroll_bar_w;
        scroll_track_h = NK_MAX(scroll_track_h - (2.0f * scroll_bar_w), 0.0f);
    }

    if (target <= scroll_track_h || scroll_track_h <= 0.0f)
        return list_bounds.y + (list_bounds.h * 0.5f);

    float scroll_offset = NK_CLAMP(0.0f, (float)scroll_y, target - scroll_track_h);
    float scroll_ratio = scroll_track_h / target;
    float scroll_off = scroll_offset / target;
    float cursor_h = NK_MAX((scroll_ratio * scroll_track_h) -
                            (2.0f * style->border + 2.0f * style->padding.y),
                            0.0f);
    float cursor_y = scroll_track_y + (scroll_off * scroll_track_h) +
                     style->border_cursor + style->padding.y;

    return cursor_y + (cursor_h * 0.5f);
}

/* Push the launcher-local yellow selection theme onto the selectable style.
 * Affects only the ROM row loop; pop in strict reverse order. */
static void launcher_push_rom_list_selectable_style(struct nk_context *ctx)
{
    struct nk_style_selectable *s = &ctx->style.selectable;
    struct nk_style_item sel_bg = nk_style_item_color(g_theme.selection_bg);
    struct nk_style_item clear  = nk_style_item_color(nk_rgba(0, 0, 0, 0));

    /* Selected-row backgrounds */
    nk_style_push_style_item(ctx, &s->normal_active,  sel_bg);
    nk_style_push_style_item(ctx, &s->hover_active,   sel_bg);
    nk_style_push_style_item(ctx, &s->pressed_active, sel_bg);

    /* Inactive-row backgrounds: no hover/press highlight, only selected row */
    nk_style_push_style_item(ctx, &s->hover,   clear);
    nk_style_push_style_item(ctx, &s->pressed, clear);

    /* Selected-row text */
    nk_style_push_color(ctx, &s->text_normal_active,  g_theme.selection);
    nk_style_push_color(ctx, &s->text_hover_active,   g_theme.selection);
    nk_style_push_color(ctx, &s->text_pressed_active, g_theme.selection);
}

static void launcher_pop_rom_list_selectable_style(struct nk_context *ctx)
{
    /* Reverse order of the pushes above: 3 colors, 5 style items. */
    nk_style_pop_color(ctx);
    nk_style_pop_color(ctx);
    nk_style_pop_color(ctx);

    nk_style_pop_style_item(ctx);
    nk_style_pop_style_item(ctx);
    nk_style_pop_style_item(ctx);
    nk_style_pop_style_item(ctx);
    nk_style_pop_style_item(ctx);
}

/******************************************************************************
**  Function   :  launcher_render_rom_list
**
**  Objective  :  Render the ROM list using nk_list_view
**
**  Parameters :  ctx - Nuklear context
**
**  return     :  NONE
******************************************************************************/
void launcher_render_rom_list(struct nk_context *ctx)
{
    static int last_selected = -1;
    static struct nk_list_view view;
    static unsigned int last_click_time = 0;
    static int last_click_index = -1;
    int row_height = g_theme.row_height;
    int had_mouse_wheel = (ctx->input.mouse.scroll_delta.y < -0.1f ||
                           ctx->input.mouse.scroll_delta.y > 0.1f);

    /* Measure current vertical position to compute remaining height */
    nk_layout_row_dynamic(ctx, 1, 1);
    struct nk_rect cur_pos = nk_widget_bounds(ctx);
    nk_spacing(ctx, 1);

    /* Left margin ~4 character widths */
    float left_margin = ctx->style.font->width(ctx->style.font->userdata,
                            ctx->style.font->height, "XXXX", 4);
    struct nk_rect content = nk_window_get_content_region(ctx);
    float content_w = content.w - left_margin * 1.6f;
    if (content_w < 10) content_w = 10;
    float margin_cols[2] = {left_margin, content_w};

    /* Compute list height: remaining space minus info section and bottom margin */
    float spacing_y = ctx->style.window.spacing.y;
    float avail_h = content.h - (cur_pos.y - content.y) - 1.0f;
    float top_gap  = 5.0f + spacing_y;                              /* spacing row below */
    float info_h   = (10.0f + 2.0f + 10.0f) + 7 * spacing_y       /* gaps + divider */
                   + 5.0f * (row_height + spacing_y);              /* 5 info label rows */
    float bottom_margin_h = 3.0f * row_height;
    float list_h = avail_h - top_gap - info_h - bottom_margin_h;
    if (list_h < (float)row_height) list_h = (float)row_height;

    int actual_height = row_height + (int)ctx->style.window.spacing.y;
    if (actual_height < 1)
        actual_height = 1;
    float estimated_clip_h = list_h - ctx->style.window.scrollbar_size.y;
    if (estimated_clip_h < (float)actual_height)
        estimated_clip_h = (float)actual_height;
    int fully_visible_rows = (int)(estimated_clip_h / (float)actual_height);
    if (fully_visible_rows < 1)
        fully_visible_rows = 1;

    int virtual_rom_count = g_launcher.rom_count;
    if (g_launcher.rom_count > fully_visible_rows)
        virtual_rom_count += (fully_visible_rows >= 4) ? 2 : 1;

    /* Handle vertical navigation and enter to launch */
    if (!g_modal_cfg.modal_active) {
        int selected = g_launcher.selected_rom_index;
        int page_size = (fully_visible_rows > 1) ? fully_visible_rows - 1 : 1;
        input_check_vertical_navigation(ctx, &selected, g_launcher.rom_count,
                                        page_size);
        char typeahead[64];
        if (input_check_typeahead(ctx, typeahead, sizeof(typeahead), 1000) && typeahead[0]) {
            int tlen = (int)strlen(typeahead);
            int lo = 0, hi = g_launcher.rom_count;
            while (lo < hi) {
                int mid = (lo + hi) / 2;
                if (strncasecmp(g_launcher.rom_list[mid].title, typeahead, tlen) < 0)
                    lo = mid + 1;
                else
                    hi = mid;
            }
            if (lo < g_launcher.rom_count &&
                strncasecmp(g_launcher.rom_list[lo].title, typeahead, tlen) == 0)
                selected = lo;
        }
        if (selected != g_launcher.selected_rom_index) {
            launcher_select_rom(selected);
        }
        if (input_check_select_item(ctx) && g_launcher.selected_rom_index >= 0) {
            launcher_launch_rom();
        }
    }

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

    /* ROM List with left margin */
    nk_layout_row(ctx, NK_STATIC, list_h, 2, margin_cols);
    nk_spacing(ctx, 1);

    struct nk_rect list_rect = nk_widget_bounds(ctx);
    if (nk_list_view_begin(ctx, &view, "rom_list", NK_WINDOW_BORDER, row_height, virtual_rom_count)) {
        int selected = g_launcher.selected_rom_index;
        int list_visible_rows = fully_visible_rows;
        nk_uint programmed_scroll_y;
        nk_uint final_scroll_y;
        struct nk_rect list_clip = list_rect;
        struct nk_rect list_bounds = list_rect;
        int list_cushion_rows = (list_visible_rows >= 4) ? 2 : 1;
        if (list_cushion_rows >= list_visible_rows)
            list_cushion_rows = list_visible_rows - 1;
        if (list_cushion_rows < 0)
            list_cushion_rows = 0;

        /* Auto-scroll to keep selected item visible.
         * After updating scroll_value, also update begin/end so the render loop
         * uses the correct range this frame (fixes page-jump off-screen flash). */
        if (selected >= 0 && selected != last_selected && view.count > 0) {
            int target = view.begin;
            int first_preferred = view.begin + list_cushion_rows;
            int last_preferred = view.begin + list_visible_rows - 1 - list_cushion_rows;

            if (last_preferred < view.begin)
                last_preferred = view.begin;

            if (selected < first_preferred) {
                target = selected - list_cushion_rows;
            } else if (selected > last_preferred) {
                target = selected - list_visible_rows + 1 + list_cushion_rows;
            }

            if (target < 0)
                target = 0;
            int max_begin = virtual_rom_count - list_visible_rows;
            if (max_begin < 0)
                max_begin = 0;
            if (target > max_begin)
                target = max_begin;

            view.scroll_value = target * actual_height;
            view.begin = target;
            view.end = view.begin + view.count;
            if (view.end > virtual_rom_count) view.end = virtual_rom_count;
        }

        programmed_scroll_y = view.scroll_value;

        nk_layout_row_dynamic(ctx, row_height - 0, 1);

        #define ICON_MICROCHIP "\xef\x8b\x9b"
        char icon_title[512];
        launcher_push_rom_list_selectable_style(ctx);
        for (int i = view.begin; i < view.end && i < g_launcher.rom_count; i++) {
            int is_selected = (i == selected);

            /* Build "<icon>  <title>" WITHOUT running the icon bytes through
             * snprintf.  On this device's (Android 6) bionic, the printf family
             * parses the format string as multibyte under the C locale, chokes
             * on the icon's high bytes (ICON_MICROCHIP = 0xEF 0x8B 0x9B) and
             * returns -1 having written nothing -> every list row rendered as an
             * empty label (and nk_selectable asserts on len==0 in debug builds).
             * Copying the literal prefix directly and appending the title
             * sidesteps printf's multibyte conversion entirely. */
            strcpy(icon_title, ICON_MICROCHIP "  ");
            strncat(icon_title, g_launcher.rom_list[i].title,
                    sizeof(icon_title) - strlen(icon_title) - 1);
            if (nk_selectable_label(ctx, icon_title, NK_TEXT_LEFT, &is_selected)) {
                if (i == last_click_index && (pc_get_ticks() - last_click_time) < 300) {
                    launcher_launch_rom();
                } else {
                    launcher_select_rom(i);
                }
                last_click_index = i;
                last_click_time = pc_get_ticks();
            }
        }
        launcher_pop_rom_list_selectable_style(ctx);

        nk_list_view_end(&view);
        final_scroll_y = view.scroll_pointer ? *view.scroll_pointer : programmed_scroll_y;

        if (!had_mouse_wheel && final_scroll_y != programmed_scroll_y &&
            g_launcher.rom_count > 0 &&
            (ctx->input.mouse.buttons[NK_BUTTON_LEFT].down ||
             nk_input_is_mouse_released(&ctx->input, NK_BUTTON_LEFT))) {
            float pointer_y = ctx->input.mouse.pos.y;
            int pointer_outside_y = (pointer_y < list_clip.y ||
                                     pointer_y >= (list_clip.y + list_clip.h));
            int begin = (int)(final_scroll_y / (nk_uint)actual_height);
            int pointer_row;
            int target;

            if (pointer_outside_y)
                pointer_y = launcher_scrollbar_thumb_center_y(ctx, list_bounds,
                        final_scroll_y, actual_height, virtual_rom_count);

            pointer_row = (int)((pointer_y - list_clip.y) / (float)actual_height);
            if (pointer_row < 0)
                pointer_row = 0;
            if (pointer_row >= list_visible_rows)
                pointer_row = list_visible_rows - 1;

            target = begin + pointer_row;
            if (target < 0)
                target = 0;
            if (target >= g_launcher.rom_count)
                target = g_launcher.rom_count - 1;

            if (target != g_launcher.selected_rom_index) {
                launcher_select_rom_deferred(target);
                last_click_index = -1;
                last_click_time = 0;
            }
        }
    }

    if (g_launcher.rom_profile_load_deferred &&
        !ctx->input.mouse.buttons[NK_BUTTON_LEFT].down)
        launcher_commit_deferred_profile();

    /* ROM Info section */
    nk_layout_row_dynamic(ctx, 10, 1);
    nk_spacing(ctx, 1);

    nk_layout_row(ctx, NK_STATIC, 2, 2, margin_cols);
    nk_spacing(ctx, 1);
    nk_rule_horizontal(ctx, g_theme.menu_unselected, nk_true);

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

    if (g_launcher.selected_rom_index >= 0) {
        char info_buf[256];

        /* Name */
        nk_layout_row(ctx, NK_STATIC, (float)row_height, 2, margin_cols);
        nk_spacing(ctx, 1);
        snprintf(info_buf, sizeof(info_buf), "Name: %s", g_launcher.rom_name);
        nk_label(ctx, info_buf, NK_TEXT_LEFT);

        /* CRC */
        nk_layout_row(ctx, NK_STATIC, (float)row_height, 2, margin_cols);
        nk_spacing(ctx, 1);
        snprintf(info_buf, sizeof(info_buf), "CRC: %08X", g_launcher.selected_crc);
        nk_label(ctx, info_buf, NK_TEXT_LEFT);

        /* Filename */
        nk_layout_row(ctx, NK_STATIC, (float)row_height, 2, margin_cols);
        nk_spacing(ctx, 1);
        if (g_launcher.rom_filename[0]) {
            snprintf(info_buf, sizeof(info_buf), "File: %s", g_launcher.rom_filename);
        } else {
            snprintf(info_buf, sizeof(info_buf), "File: (not found)");
        }
        nk_label(ctx, info_buf, NK_TEXT_LEFT);

        /* Size */
        nk_layout_row(ctx, NK_STATIC, (float)row_height, 2, margin_cols);
        nk_spacing(ctx, 1);
        snprintf(info_buf, sizeof(info_buf), "Size: %d bytes", g_launcher.rom_size);
        nk_label(ctx, info_buf, NK_TEXT_LEFT);

        /* Mapping */
        nk_layout_row(ctx, NK_STATIC, (float)row_height, 2, margin_cols);
        nk_spacing(ctx, 1);
        snprintf(info_buf, sizeof(info_buf), "Mapping: %s", g_launcher.rom_mapping);
        nk_label(ctx, info_buf, NK_TEXT_LEFT);
    }

    last_selected = g_launcher.selected_rom_index;
}

/******************************************************************************
**  Function   :  launcher_get_attach_to_string
**
**  Objective  :  Get string for attach_to option
**
**  Parameters :  wgt - widget
**                buf - output buffer
**
**  return     :  pointer to buf
******************************************************************************/
static char *launcher_get_attach_to_string(struct option_widget *wgt, char *buf)
{
    int *val = (int *)wgt->data;

    switch (*val) {
        case ATTACH_TO_INDEX_CART: strcpy(buf, "Cart"); break;
        case ATTACH_TO_INDEX_CASSETTE: strcpy(buf, "Cassette"); break;
        default:
            if (*val >= ATTACH_TO_INDEX_DISK_1 && *val <= ATTACH_TO_INDEX_DISK_8) {
                sprintf(buf, "Disk %d", *val - ATTACH_TO_INDEX_DISK_1 + 1);
            } else {
                strcpy(buf, "Cart");
            }
            break;
    }
    return buf;
}

/******************************************************************************
**  Function   :  launcher_get_mapping_string
**
**  Objective  :  Get string for mapping option
**
**  Parameters :  wgt - widget
**                buf - output buffer
**
**  return     :  pointer to buf
******************************************************************************/
static char *launcher_get_mapping_string(struct option_widget *wgt, char *buf)
{
    int *val = (int *)wgt->data;
    cart_get_mapping_string(*val, buf);
    return buf;
}

/******************************************************************************
**  Function   :  launcher_get_view_string
**
**  Objective  :  Get string for view option
**
**  Parameters :  wgt - widget
**                buf - output buffer
**
**  return     :  pointer to buf
******************************************************************************/
static char *launcher_get_view_string(struct option_widget *wgt, char *buf)
{
    int *val = (int *)wgt->data;

    if (*val >= 0 && *val < g_launcher.csv_file_count) {
        strcpy(buf, g_launcher.csv_files[*val]);
    } else {
        strcpy(buf, "All");
    }
    return buf;
}

/******************************************************************************
**  Function   :  launcher_get_media_type
**
**  Objective  :  Convert attach_to index to e_cart_type for media_if_open_rom
**
**  Parameters :  attach_index - ATTACH_TO_INDEX_* value
**                slot - pointer to store disk slot number (for disks)
**
**  return     :  e_cart_type value (CART_CART, CART_CASSETTE, or CART_FLOPPY)
******************************************************************************/
static int launcher_get_media_type(int attach_index, int *slot)
{
    if (attach_index == ATTACH_TO_INDEX_CART) {
        *slot = 0;
        return CART_CART;
    } else if (attach_index == ATTACH_TO_INDEX_CASSETTE) {
        *slot = 0;
        return CART_CASSETTE;
    } else if (attach_index >= ATTACH_TO_INDEX_DISK_1 && attach_index <= ATTACH_TO_INDEX_DISK_8) {
        *slot = attach_index - ATTACH_TO_INDEX_DISK_1;
        return CART_FLOPPY;
    }
    /* Default to cart */
    *slot = 0;
    return CART_CART;
}

/******************************************************************************
**  Function   :  launcher_action_launch
**
**  Objective  :  Launch selected ROM with reset
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_launch(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    launcher_launch_rom();
}

/******************************************************************************
**  Function   :  launcher_action_launch_no_reset
**
**  Objective  :  Launch selected ROM without reset
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_launch_no_reset(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;

    if (g_launcher.selected_rom_index < 0)
        return;

    launcher_commit_deferred_profile();

    if (!g_launcher.rom_filename[0]) {
        struct modal_config *cfg = guitop_get_modal_cfg();
        memset(cfg, 0, sizeof(struct modal_config));
        cfg->modal_type = MODAL_MESSAGE;
        cfg->modal_active = 1;
        cfg->msg_type = KAT_MSG_TYPE_WARN;
        strcpy(cfg->title, "Launch ROM");
        strcpy(cfg->cancel_btn_label, "OK");
        snprintf(cfg->msg, sizeof(cfg->msg), "No filename set for this ROM");
        message_init(cfg);
        return;
    }

    util_alloc_string(&config_get_global_cfg()->general.rom, g_launcher.rom_filename);

    /* The selected profile is only needed while the launcher is visible. */
    if (g_launcher.rom_profile) {
        toml_free_node_tree(g_launcher.rom_profile);
        g_launcher.rom_profile = NULL;
    }

    gui_launch(3);
}

/******************************************************************************
**  Function   :  launcher_action_attach
**
**  Objective  :  Attach selected ROM without launching
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_attach(struct option_widget *wgt, void *data)
{
    int media_type, slot;
    (void)wgt; (void)data;

    if (g_launcher.selected_rom_index < 0)
        return;

    launcher_commit_deferred_profile();

    if (g_launcher.rom_filename[0]) {
        media_type = launcher_get_media_type(g_launcher.attach_to, &slot);
        media_if_open_rom(g_launcher.rom_filename, media_type, slot, 0);
    }
}

/******************************************************************************
**  Function   :  launcher_action_save
**
**  Objective  :  Save current ROM settings
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_save(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    if (g_launcher.selected_rom_index < 0)
        return;

    launcher_commit_deferred_profile();

    unsigned long save_crc = g_launcher.selected_crc;
    char old_title[128];
    strncpy(old_title, g_launcher.rom_list[g_launcher.selected_rom_index].title,
            sizeof(old_title) - 1);
    old_title[sizeof(old_title) - 1] = '\0';

    /* Check for CRC change if a file is set */
    if (g_launcher.rom_filename[0]) {
        t_media tmp_media;
        media_init(&tmp_media);
        if (media_if_create_from_file(g_launcher.rom_filename, &tmp_media) == 0) {
            if (tmp_media.crc && tmp_media.crc != g_launcher.selected_crc) {
                save_crc = tmp_media.crc;
                g_launcher.selected_crc = save_crc;
                g_launcher.rom_list[g_launcher.selected_rom_index].crc = (unsigned int)save_crc;
            }
        }
        media_exit(&tmp_media);
    }

    t_media media;
    media_init(&media);

    media.title = g_launcher.rom_name;
    media.file  = g_launcher.rom_filename[0] ? g_launcher.rom_filename : "";
    media.crc = save_crc;
    media.mapping = g_launcher.mapping_index;
    media.machine_type = config_get_global_cfg()->general.machine;

    unsigned long long mask = KCFG_MEDIA_TITLE | KCFG_MEDIA_FILE | KCFG_MEDIA_MAPPING;
    config_save_media_settings((unsigned int)save_crc, &media, mask);

    /* Update the in-memory rom_list title */
    strncpy(g_launcher.rom_list[g_launcher.selected_rom_index].title,
            g_launcher.rom_name,
            sizeof(g_launcher.rom_list[0].title) - 1);
    g_launcher.rom_list[g_launcher.selected_rom_index].title[
            sizeof(g_launcher.rom_list[0].title) - 1] = '\0';

    /* Propagate title change to favorites.csv */
    if (strcmp(old_title, g_launcher.rom_name) != 0)
        launcher_update_favorites_title(save_crc, g_launcher.rom_name);

    guitop_show_toast_msg("ROM settings saved.");
}

/******************************************************************************
**  Function   :  launcher_action_clear_custom
**
**  Objective  :  Clear custom settings for ROM
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_clear_custom(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    if (g_launcher.selected_rom_index < 0)
        return;

    launcher_commit_deferred_profile();

    config_reset_media_custom_settings(g_launcher.selected_crc);
    launcher_select_rom(g_launcher.selected_rom_index);
    guitop_show_toast_msg("Custom settings cleared.");
}

/******************************************************************************
**  Function   :  launcher_get_favorites_path
**
**  Objective  :  Return allocated path to favorites.csv for current machine
**
**  Parameters :  NONE
**
**  return     :  Allocated string (caller must free), or NULL on error
******************************************************************************/
static char *launcher_get_favorites_path(void)
{
    char *profiles_dir = config_get_profiles_dir(config_get_global_cfg()->general.machine);
    if (!profiles_dir)
        return NULL;

    char *temp = util_make_path_alloc(profiles_dir, "favorites.csv");
    free(profiles_dir);
    if (!temp)
        return NULL;

    char *full_path = util_create_program_path_alloc(temp);
    free(temp);
    return full_path;
}

/******************************************************************************
**  Function   :  launcher_is_favorite
**
**  Objective  :  Check if a CRC is in favorites.csv
**
**  Parameters :  crc - CRC to look for
**
**  return     :  1 if found, 0 otherwise
******************************************************************************/
static int launcher_is_favorite(unsigned int crc)
{
    char *full_path = launcher_get_favorites_path();
    if (!full_path)
        return 0;

    int found = 0;
    FILE *fp = fopen(full_path, "r");
    if (fp) {
        char line[256];
        while (fgets(line, sizeof(line), fp)) {
            char title[sizeof(g_launcher.rom_list[0].title)];
            unsigned int entry_crc;

            if (launcher_parse_csv_entry(line, title, sizeof(title), &entry_crc) &&
                    entry_crc == crc) {
                found = 1;
                break;
            }
        }
        fclose(fp);
    }

    free(full_path);
    return found;
}

/******************************************************************************
**  Function   :  launcher_action_add_favorite
**
**  Objective  :  Add selected ROM to favorites
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_add_favorite(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    if (g_launcher.selected_rom_index < 0)
        return;

    char *full_path = launcher_get_favorites_path();
    if (!full_path)
        return;

    /* Bail out silently if already a favorite */
    if (launcher_is_favorite(g_launcher.selected_crc)) {
        free(full_path);
        return;
    }

    /* Ensure the profiles directory exists */
    {
        char *dir_copy = strdup(full_path);
        if (dir_copy) {
            char *sep = strrchr(dir_copy, util_get_dir_seperator_character());
            if (sep) {
                *sep = '\0';
                util_mkdir(dir_copy, UTIL_DEFAULT_DIR_MODE);  /* OK if already exists */
            }
            free(dir_copy);
        }
    }

    /* Append to favorites */
    FILE *fp = fopen(full_path, "a");
    if (fp) {
        launcher_write_csv_entry(fp, g_launcher.rom_list[g_launcher.selected_rom_index].title,
                g_launcher.selected_crc);
        fclose(fp);
        guitop_show_toast_msg("Added to favorites.");
    }

    free(full_path);
}

/******************************************************************************
**  Function   :  launcher_action_remove_favorite
**
**  Objective  :  Remove selected ROM from favorites
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_remove_favorite(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    if (g_launcher.selected_rom_index < 0)
        return;

    char *full_path = launcher_get_favorites_path();
    if (!full_path)
        return;

    FILE *fp = fopen(full_path, "r");
    if (!fp) {
        free(full_path);
        return;
    }

    /* Read all lines that don't match our CRC */
    int capacity = 32, count = 0, removed = 0;
    char **lines = malloc(capacity * sizeof(char *));
    if (!lines) {
        fclose(fp);
        free(full_path);
        return;
    }

    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n')
            line[len - 1] = '\0';
        if (!line[0])
            continue;

        char parse_line[256];
        char title[sizeof(g_launcher.rom_list[0].title)];
        unsigned int entry_crc;
        strncpy(parse_line, line, sizeof(parse_line) - 1);
        parse_line[sizeof(parse_line) - 1] = '\0';

        if (launcher_parse_csv_entry(parse_line, title, sizeof(title), &entry_crc) &&
                entry_crc == g_launcher.selected_crc) {
            removed = 1;
            continue;  /* Skip this entry */
        }

        if (count >= capacity) {
            capacity *= 2;
            char **tmp = realloc(lines, capacity * sizeof(char *));
            if (!tmp)
                break;
            lines = tmp;
        }
        lines[count++] = strdup(line);
    }
    fclose(fp);

    if (removed) {
        fp = fopen(full_path, "w");
        if (fp) {
            for (int i = 0; i < count; i++)
                fprintf(fp, "%s\n", lines[i]);
            fclose(fp);
        }
        guitop_show_toast_msg("Removed from favorites.");
    }

    for (int i = 0; i < count; i++)
        free(lines[i]);
    free(lines);
    free(full_path);
}

/******************************************************************************
**  Function   :  launcher_current_csv_name
**
**  Objective  :  Return the CSV filename for the currently displayed view
**
**  Parameters :  NONE
**
**  return     :  Static string (do not free)
******************************************************************************/
static const char *launcher_current_csv_name(void)
{
    static char buf[256];
    int index = g_launcher.current_csv_index;
    if (index == 0)
        return "list.csv";
    if (index == 1)
        return "favorites.csv";
    if (index < g_launcher.csv_file_count) {
        snprintf(buf, sizeof(buf), "%s.csv", g_launcher.csv_files[index]);
        return buf;
    }
    return "list.csv";
}

/******************************************************************************
**  Function   :  launcher_remove_from_all_csvs
**
**  Objective  :  Remove all entries matching crc from every CSV in the
**                profiles directory
**
**  Parameters :  crc - CRC to remove
**
**  return     :  NONE
******************************************************************************/
static void launcher_remove_from_all_csvs(unsigned int crc)
{
    char *profiles_dir = config_get_profiles_dir(config_get_global_cfg()->general.machine);
    if (!profiles_dir)
        return;

    char *full_dir = util_create_program_path_alloc(profiles_dir);
    free(profiles_dir);
    if (!full_dir)
        return;

    DIR *dir = opendir(full_dir);
    if (!dir) {
        free(full_dir);
        return;
    }

    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        size_t len = strlen(entry->d_name);
        if (len <= 4 || strcmp(entry->d_name + len - 4, ".csv") != 0)
            continue;

        char *csv_path = util_make_path_alloc(full_dir, entry->d_name);
        if (!csv_path)
            continue;

        FILE *fp = fopen(csv_path, "r");
        if (!fp) {
            free(csv_path);
            continue;
        }

        int capacity = 32, count = 0, removed = 0;
        char **lines = malloc(capacity * sizeof(char *));
        if (!lines) {
            fclose(fp);
            free(csv_path);
            continue;
        }

        char line[256];
        while (fgets(line, sizeof(line), fp)) {
            size_t llen = strlen(line);
            if (llen > 0 && line[llen - 1] == '\n')
                line[llen - 1] = '\0';
            if (!line[0])
                continue;

            char parse_line[256];
            char title[sizeof(g_launcher.rom_list[0].title)];
            unsigned int entry_crc;
            strncpy(parse_line, line, sizeof(parse_line) - 1);
            parse_line[sizeof(parse_line) - 1] = '\0';

            if (launcher_parse_csv_entry(parse_line, title, sizeof(title), &entry_crc) &&
                    entry_crc == crc) {
                removed = 1;
                continue;
            }

            if (count >= capacity) {
                capacity *= 2;
                char **tmp = realloc(lines, capacity * sizeof(char *));
                if (!tmp)
                    break;
                lines = tmp;
            }
            lines[count++] = strdup(line);
        }
        fclose(fp);

        if (removed) {
            fp = fopen(csv_path, "w");
            if (fp) {
                for (int i = 0; i < count; i++)
                    fprintf(fp, "%s\n", lines[i]);
                fclose(fp);
            }
        }

        for (int i = 0; i < count; i++)
            free(lines[i]);
        free(lines);
        free(csv_path);
    }

    closedir(dir);
    free(full_dir);
}

/******************************************************************************
**  Function   :  launcher_delete_confirmed
**
**  Objective  :  Callback after delete confirmation modal — performs deletion
**
**  Parameters :  cfg - modal config with result
**
**  return     :  NONE
******************************************************************************/
static void launcher_delete_confirmed(struct modal_config *cfg)
{
    if (!cfg->result)
        return;

    unsigned int crc = g_launcher.selected_crc;
    int new_index = g_launcher.selected_rom_index;

    media_if_delete_crc(crc);
    launcher_remove_from_all_csvs(crc);

    launcher_load_rom_list(launcher_current_csv_name());

    if (g_launcher.rom_count == 0) {
        g_launcher.selected_rom_index = -1;
    } else {
        if (new_index >= g_launcher.rom_count)
            new_index = g_launcher.rom_count - 1;
        launcher_select_rom(new_index);
    }

    guitop_show_toast_msg("ROM deleted.");
}

/******************************************************************************
**  Function   :  launcher_action_delete
**
**  Objective  :  Show confirmation modal then delete the selected ROM profile
**
**  Parameters :  wgt  - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_delete(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    if (g_launcher.selected_rom_index < 0)
        return;

    launcher_commit_deferred_profile();

    struct modal_config *cfg = guitop_get_modal_cfg();
    memset(cfg, 0, sizeof(struct modal_config));
    cfg->modal_type   = MODAL_MESSAGE_CHOOSE;
    cfg->modal_active = 1;
    cfg->msg_type     = KAT_MSG_TYPE_WARN;
    strcpy(cfg->title, "Delete ROM Profile");
    snprintf(cfg->msg, sizeof(cfg->msg),
             "Delete profile for \"%s\"? This cannot be undone.",
             g_launcher.rom_name);
    strcpy(cfg->accept_btn_label, "Delete");
    strcpy(cfg->cancel_btn_label, "Cancel");
    cfg->post_show = launcher_delete_confirmed;
    message_init(cfg);
}

/******************************************************************************
**  Function   :  launcher_action_clear_filename
**
**  Objective  :  Clear filename from ROM profile
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_clear_filename(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;

    if (g_launcher.selected_rom_index < 0)
        return;

    launcher_commit_deferred_profile();

    t_media media;
    media_init(&media);

    media.file = "";
    media.crc = g_launcher.selected_crc;
    media.machine_type = config_get_global_cfg()->general.machine;

    config_save_media_settings(g_launcher.selected_crc, &media, KCFG_MEDIA_FILE);

    /* Reload info */
    launcher_select_rom(g_launcher.selected_rom_index);
}

/******************************************************************************
**  Function   :  launcher_update_favorites_title
**
**  Objective  :  Update the title for a CRC entry in favorites.csv
**
**  Parameters :  crc       - CRC to find
**                new_title - new title string to write
**
**  return     :  NONE
******************************************************************************/
static void launcher_update_favorites_title(unsigned long crc, const char *new_title)
{
    char *profiles_dir = config_get_profiles_dir(config_get_global_cfg()->general.machine);
    if (!profiles_dir)
        return;

    char *temp = util_make_path_alloc(profiles_dir, "favorites.csv");
    free(profiles_dir);
    if (!temp)
        return;

    char *full_path = util_create_program_path_alloc(temp);
    free(temp);
    if (!full_path)
        return;

    FILE *fp = fopen(full_path, "r");
    if (!fp) {
        free(full_path);
        return;
    }

    /* Read all lines */
    int capacity = 32, count = 0, updated = 0;
    char **lines = malloc(capacity * sizeof(char *));
    if (!lines) {
        fclose(fp);
        free(full_path);
        return;
    }

    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        size_t len = strlen(line);
        if (len > 0 && line[len - 1] == '\n')
            line[len - 1] = '\0';
        if (!line[0])
            continue;

        char parse_line[256];
        char title[sizeof(g_launcher.rom_list[0].title)];
        unsigned int entry_crc;
        strncpy(parse_line, line, sizeof(parse_line) - 1);
        parse_line[sizeof(parse_line) - 1] = '\0';

        if (launcher_parse_csv_entry(parse_line, title, sizeof(title), &entry_crc) &&
                entry_crc == crc) {
            char new_line[256];
            snprintf(new_line, sizeof(new_line), "\"%s\", 0x%08lx", new_title, crc);
            lines[count] = strdup(new_line);
            updated = 1;
        } else {
            lines[count] = strdup(line);
        }

        if (++count >= capacity) {
            capacity *= 2;
            char **tmp = realloc(lines, capacity * sizeof(char *));
            if (!tmp)
                break;
            lines = tmp;
        }
    }
    fclose(fp);

    if (updated) {
        fp = fopen(full_path, "w");
        if (fp) {
            for (int i = 0; i < count; i++)
                fprintf(fp, "%s\n", lines[i]);
            fclose(fp);
        }
    }

    for (int i = 0; i < count; i++)
        free(lines[i]);
    free(lines);
    free(full_path);
}

/******************************************************************************
**  Function   :  launcher_edit_name_result
**
**  Objective  :  post_show callback after the Name text-input modal closes
**
**  Parameters :  cfg - modal config
**
**  return     :  NONE
******************************************************************************/
static void launcher_edit_name_result(struct modal_config *cfg)
{
    if (!cfg->result)
        return;

    strncpy(g_launcher.rom_name, cfg->edit_buf, sizeof(g_launcher.rom_name) - 1);
    g_launcher.rom_name[sizeof(g_launcher.rom_name) - 1] = '\0';
}

/******************************************************************************
**  Function   :  launcher_action_edit_name
**
**  Objective  :  Open a text-input modal to edit the ROM name
**
**  Parameters :  wgt  - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_edit_name(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    if (g_launcher.selected_rom_index < 0)
        return;

    launcher_commit_deferred_profile();

    struct modal_config *cfg = guitop_get_modal_cfg();
    memset(cfg, 0, sizeof(struct modal_config));
    cfg->modal_type = MODAL_TEXT_INPUT;
    cfg->modal_active = 1;
    strcpy(cfg->title, "Edit Name");
    strcpy(cfg->cancel_btn_label, "Cancel");
    strcpy(cfg->accept_btn_label, "OK");
    snprintf(cfg->msg, sizeof(cfg->msg), "Enter ROM name:");
    strncpy(cfg->edit_buf, g_launcher.rom_name, sizeof(cfg->edit_buf) - 1);
    cfg->edit_len = (int)strlen(cfg->edit_buf);
    cfg->post_show = launcher_edit_name_result;
    message_init(cfg);
}

/******************************************************************************
**  Function   :  launcher_file_result
**
**  Objective  :  post_show callback after the file browser closes for ROM file
**
**  Parameters :  cfg - modal config
**
**  return     :  NONE
******************************************************************************/
static void launcher_file_result(struct modal_config *cfg)
{
    if (!cfg->result)
        return;

    strncpy(g_launcher.rom_filename, filebrowser_result(),
            sizeof(g_launcher.rom_filename) - 1);
    g_launcher.rom_filename[sizeof(g_launcher.rom_filename) - 1] = '\0';
}

/******************************************************************************
**  Function   :  launcher_action_set_file
**
**  Objective  :  Open a file browser to set the ROM filename
**
**  Parameters :  wgt  - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_set_file(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;
    if (g_launcher.selected_rom_index < 0)
        return;

    launcher_commit_deferred_profile();

    struct modal_config *cfg = guitop_get_modal_cfg();
    memset(cfg, 0, sizeof(struct modal_config));
    cfg->modal_type = MODAL_FB_LOAD;
    cfg->modal_active = 2;
    cfg->data = g_launcher.rom_filename[0] ? g_launcher.rom_filename : NULL;
    strcpy(cfg->title, "Select ROM File");
    cfg->post_show = launcher_file_result;
    filebrowser_init(cfg);
}

/******************************************************************************
**  Function   :  launcher_on_view_change
**
**  Objective  :  Handle view change (CSV selection)
**
**  Parameters :  wgt - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_on_view_change(struct option_widget *wgt, void *data)
{
    int index = g_launcher.current_csv_index;
    const char *csv_file;

    if (index == 0) {
        csv_file = "list.csv";
    } else if (index == 1) {
        csv_file = "favorites.csv";
    } else if (index < g_launcher.csv_file_count) {
        static char buf[256];
        snprintf(buf, sizeof(buf), "%s.csv", g_launcher.csv_files[index]);
        csv_file = buf;
    } else {
        csv_file = "list.csv";
    }

    launcher_load_rom_list(csv_file);

    if (g_launcher.rom_count > 0) {
        launcher_select_rom(0);
    } else {
        g_launcher.selected_rom_index = -1;
    }
}

/* Static buffers for ROM info strings displayed in menus */
static char info_crc[16];
static char info_file[256];
static char info_size[64];
static char info_mapping[64];

/******************************************************************************
**  Function   :  launcher_add_rom_info_widgets
**
**  Objective  :  Append ROM info strings to a menu widget array
**
**  Parameters :  wid_ary   - array of widgets to populate
**                num_items - current number of items in array
**
**  return     :  Updated number of menu items
******************************************************************************/
static int launcher_add_rom_info_widgets(struct option_widget *wid_ary, int num_items)
{
    if (g_launcher.selected_rom_index < 0)
        return num_items;

    launcher_commit_deferred_profile();

    /* Divider before ROM info */
    wid_ary[num_items].divider = 1;

    /* Name */
    strcpy(wid_ary[num_items].label, "Name");
    wid_ary[num_items].widget_type = WIDGET_TYPE_STRING;
    wid_ary[num_items].data = g_launcher.rom_name;
    num_items++;

    /* CRC */
    snprintf(info_crc, sizeof(info_crc), "%08X", g_launcher.selected_crc);
    strcpy(wid_ary[num_items].label, "CRC");
    wid_ary[num_items].widget_type = WIDGET_TYPE_STRING;
    wid_ary[num_items].data = info_crc;
    num_items++;

    /* Filename */
    if (g_launcher.rom_filename[0]) {
        strncpy(info_file, g_launcher.rom_filename, sizeof(info_file) - 1);
        info_file[sizeof(info_file) - 1] = '\0';
    } else {
        strcpy(info_file, "(not found)");
    }
    strcpy(wid_ary[num_items].label, "File");
    wid_ary[num_items].widget_type = WIDGET_TYPE_STRING;
    wid_ary[num_items].data = info_file;
    num_items++;

    /* Size */
    snprintf(info_size, sizeof(info_size), "%d bytes", g_launcher.rom_size);
    strcpy(wid_ary[num_items].label, "Size");
    wid_ary[num_items].widget_type = WIDGET_TYPE_STRING;
    wid_ary[num_items].data = info_size;
    num_items++;

    /* Mapping */
    snprintf(info_mapping, sizeof(info_mapping), "%s", g_launcher.rom_mapping);
    strcpy(wid_ary[num_items].label, "Mapping");
    wid_ary[num_items].widget_type = WIDGET_TYPE_STRING;
    wid_ary[num_items].data = info_mapping;
    num_items++;

    return num_items;
}

/******************************************************************************
**  Function   :  launcher_add_from_file_name_result
**
**  Objective  :  post_show callback after name input for "Add from File" flow;
**                creates the profile and adds the ROM to list.csv
**
**  Parameters :  cfg - modal config
**
**  return     :  NONE
******************************************************************************/
static void launcher_add_from_file_name_result(struct modal_config *cfg)
{
    if (!cfg->result)
        return;

    t_media media;
    media_init(&media);
    media.title        = cfg->edit_buf;
    media.file         = s_add_file_path;
    media.crc          = s_add_file_crc;
    media.image_size   = s_add_file_size;
    media.md5          = s_add_file_md5;
    media.sha1         = s_add_file_sha1;
    media.mapping      = CART_MIN;
    media.machine_type = config_get_global_cfg()->general.machine;

    /* This profile will be refreshed after the list reload below. */
    if (g_launcher.rom_profile) {
        toml_free_node_tree(g_launcher.rom_profile);
        g_launcher.rom_profile = NULL;
    }

    unsigned long long mask = KCFG_MEDIA_TITLE | KCFG_MEDIA_FILE | KCFG_MEDIA_CRC |
                              KCFG_MEDIA_MD5 | KCFG_MEDIA_SHA1 | KCFG_MEDIA_SIZE |
                              KCFG_MEDIA_MAPPING | KCFG_MEDIA_MACHINE;
    if (config_save_media_settings(s_add_file_crc, &media, mask) != 0) {
        guitop_show_toast_msg("Failed to save ROM profile.");
        return;
    }

    launcher_load_rom_list("list.csv");

    for (int i = 0; i < g_launcher.rom_count; i++) {
        if (g_launcher.rom_list[i].crc == s_add_file_crc) {
            launcher_select_rom(i);
            break;
        }
    }

    guitop_show_toast_msg("ROM added.");
}

/******************************************************************************
**  Function   :  launcher_add_from_file_fb_result
**
**  Objective  :  post_show callback after file browser for "Add from File";
**                computes CRC and opens name input modal
**
**  Parameters :  cfg - modal config
**
**  return     :  NONE
******************************************************************************/
static void launcher_add_from_file_fb_result(struct modal_config *cfg)
{
    if (!cfg->result)
        return;

    strncpy(s_add_file_path, filebrowser_result(), sizeof(s_add_file_path) - 1);
    s_add_file_path[sizeof(s_add_file_path) - 1] = '\0';

    t_media tmp_media;
    media_init(&tmp_media);
    if (media_if_create_from_file(s_add_file_path, &tmp_media) != 0) {
        guitop_show_toast_msg("Could not read file.");
        media_exit(&tmp_media);
        return;
    }
    s_add_file_crc  = (unsigned int)tmp_media.crc;
    s_add_file_size = tmp_media.image_size;
    snprintf(s_add_file_md5, sizeof(s_add_file_md5), "%s",
             tmp_media.md5 ? tmp_media.md5 : "");
    snprintf(s_add_file_sha1, sizeof(s_add_file_sha1), "%s",
             tmp_media.sha1 ? tmp_media.sha1 : "");

    char default_name[128] = "";
    if (tmp_media.title)
        strncpy(default_name, tmp_media.title, sizeof(default_name) - 1);
    media_exit(&tmp_media);

    struct modal_config *ncfg = guitop_get_modal_cfg();
    memset(ncfg, 0, sizeof(struct modal_config));
    ncfg->modal_type = MODAL_TEXT_INPUT;
    ncfg->modal_active = 1;
    strcpy(ncfg->title, "Add ROM");
    strcpy(ncfg->cancel_btn_label, "Cancel");
    strcpy(ncfg->accept_btn_label, "OK");
    snprintf(ncfg->msg, sizeof(ncfg->msg), "Enter ROM name:");
    strncpy(ncfg->edit_buf, default_name, sizeof(ncfg->edit_buf) - 1);
    ncfg->edit_len  = (int)strlen(ncfg->edit_buf);
    ncfg->post_show = launcher_add_from_file_name_result;
    message_init(ncfg);
}

/******************************************************************************
**  Function   :  launcher_action_add_from_file
**
**  Objective  :  Open a file browser to start the "Add from File" flow
**
**  Parameters :  wgt  - widget
**                data - user data
**
**  return     :  NONE
******************************************************************************/
static void launcher_action_add_from_file(struct option_widget *wgt, void *data)
{
    (void)wgt; (void)data;

    struct modal_config *cfg = guitop_get_modal_cfg();
    memset(cfg, 0, sizeof(struct modal_config));
    cfg->modal_type   = MODAL_FB_LOAD;
    cfg->modal_active = 2;
    cfg->data         = NULL;
    strcpy(cfg->title, "Add ROM from File");
    cfg->post_show    = launcher_add_from_file_fb_result;
    filebrowser_init(cfg);
}

/******************************************************************************
**  Function   :  launcher_build_menu
**
**  Objective  :  Build menu widget array based on current section
**
**  Parameters :  wid_ary - array of widgets to populate
**
**  return     :  Number of menu items
******************************************************************************/
int launcher_build_menu(struct option_widget *wid_ary)
{
    int num_items = 0;

    /* Initialize all items */
    for (int i = 0; i < MAX_OPTIONS; i++) {
        memset(&wid_ary[i], 0, sizeof(struct option_widget));
        wid_ary[i].menu_prev = KAT_MENU_MAIN;
    }

    switch (g_launcher.current_section) {
        case LAUNCHER_SECTION_ROM_LIST:
            /* ROM list is rendered separately, but we need menu_prev set for ESC to work */
            wid_ary[0].menu_prev = KAT_MENU_MAIN;
            return 0;

        case LAUNCHER_SECTION_ROM_ACTIONS:
            /* Attach To selector */
            strcpy(wid_ary[num_items].label, "Attach To");
            wid_ary[num_items].widget_type = WIDGET_TYPE_SELECT;
            wid_ary[num_items].data_type = WIDGET_DATA_TYPE_INT;
            wid_ary[num_items].data = &g_launcher.attach_to;
            wid_ary[num_items].min = ATTACH_TO_INDEX_CART;
            wid_ary[num_items].max = ATTACH_TO_INDEX_DISK_8;
            wid_ary[num_items].get_string = launcher_get_attach_to_string;
            num_items++;

            /* Divider before actions */
            wid_ary[num_items].divider = 1;

            /* Launch */
            strcpy(wid_ary[num_items].label, "Launch");
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            wid_ary[num_items].run_task = launcher_action_launch;
            num_items++;

            /* Launch - No Reset */
            strcpy(wid_ary[num_items].label, "Launch - No Reset");
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            wid_ary[num_items].run_task = launcher_action_launch_no_reset;
            num_items++;

            /* Attach */
            strcpy(wid_ary[num_items].label, "Attach");
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            wid_ary[num_items].run_task = launcher_action_attach;
            num_items++;

            /* ROM info */
            num_items = launcher_add_rom_info_widgets(wid_ary, num_items);
            break;

        case LAUNCHER_SECTION_ROM_OPTIONS:
            launcher_commit_deferred_profile();

            /* Name — editable via text-input modal */
            strcpy(wid_ary[num_items].label, "Name");
            wid_ary[num_items].widget_type = WIDGET_TYPE_STRING;
            wid_ary[num_items].data = g_launcher.rom_name;
            wid_ary[num_items].run_task = launcher_action_edit_name;
            num_items++;

            /* File — editable via file browser */
            if (g_launcher.rom_filename[0]) {
                strncpy(info_file, g_launcher.rom_filename, sizeof(info_file) - 1);
                info_file[sizeof(info_file) - 1] = '\0';
            } else {
                strcpy(info_file, "(not set)");
            }
            strcpy(wid_ary[num_items].label, "File");
            wid_ary[num_items].widget_type = WIDGET_TYPE_STRING;
            wid_ary[num_items].data = info_file;
            wid_ary[num_items].run_task = launcher_action_set_file;
            num_items++;

            /* Mapping selector */
            strcpy(wid_ary[num_items].label, "Mapping");
            wid_ary[num_items].widget_type = WIDGET_TYPE_SELECT;
            wid_ary[num_items].data_type = WIDGET_DATA_TYPE_INT;
            wid_ary[num_items].data = &g_launcher.mapping_index;
            if (config_get_global_cfg()->general.machine == MACHINE_TYPE_5200) {
                wid_ary[num_items].min = CART_MIN;
                wid_ary[num_items].max = CART_512K_HYBRID;
            } else {
                wid_ary[num_items].min = CART_PC_8K;
                wid_ary[num_items].max = CART_BASIC;
            }
            wid_ary[num_items].get_string = launcher_get_mapping_string;
            num_items++;

            /* Divider before actions */
            wid_ary[num_items].divider = 1;

            /* Save */
            strcpy(wid_ary[num_items].label, "\xef\x83\x87  Save");
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            wid_ary[num_items].run_task = launcher_action_save;
            num_items++;

            /* Delete */
            strcpy(wid_ary[num_items].label, "\xef\x87\xb8  Delete");
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            wid_ary[num_items].run_task = launcher_action_delete;
            num_items++;

            /* Add / Remove Favorites - with divider before */
            wid_ary[num_items].divider = 1;
            if (g_launcher.selected_rom_index >= 0 &&
                launcher_is_favorite(g_launcher.selected_crc)) {
                strcpy(wid_ary[num_items].label, "Remove from Favorites");
                wid_ary[num_items].run_task = launcher_action_remove_favorite;
            } else {
                strcpy(wid_ary[num_items].label, "Add to Favorites");
                wid_ary[num_items].run_task = launcher_action_add_favorite;
            }
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            num_items++;

            /* Clear Custom Settings */
            strcpy(wid_ary[num_items].label, "Clear Custom Settings");
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            wid_ary[num_items].run_task = launcher_action_clear_custom;
            num_items++;

            /* Clear Filename */
            strcpy(wid_ary[num_items].label, "Clear Filename");
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            wid_ary[num_items].run_task = launcher_action_clear_filename;
            num_items++;

            /* Add from File */
            wid_ary[num_items].divider = 1;
            strcpy(wid_ary[num_items].label, "Add new ROM entry from File");
            wid_ary[num_items].widget_type = WIDGET_TYPE_LABEL;
            wid_ary[num_items].run_task = launcher_action_add_from_file;
            num_items++;
            break;

        case LAUNCHER_SECTION_VIEW_OPTIONS:
            /* View by ROM type */
            strcpy(wid_ary[num_items].label, "View by ROM type");
            wid_ary[num_items].widget_type = WIDGET_TYPE_SELECT;
            wid_ary[num_items].data_type = WIDGET_DATA_TYPE_INT;
            wid_ary[num_items].data = &g_launcher.current_csv_index;
            wid_ary[num_items].min = 0;
            wid_ary[num_items].max = g_launcher.csv_file_count - 1;
            wid_ary[num_items].get_string = launcher_get_view_string;
            wid_ary[num_items].run_task = launcher_on_view_change;
            num_items++;
            break;
    }

    return num_items;
}
