#include "modal.h"
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#ifdef _WIN32
    #include <windows.h>
    #include <direct.h>
    #define getcwd _getcwd
#else
    #include <unistd.h>
    #include <time.h>
#endif
#include <dirent.h>
#include <sys/stat.h>
#define NK_INCLUDE_FIXED_TYPES
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_DEFAULT_ALLOCATOR
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT
#include "nuklear.h"
#include "filebrowser.h"
#include "../interface/sdl_if.h"
#include "../interface/katconfig.h"
#include "../core/cart.h"
#include "theme.h"
#include "widget.h"

/* Forward declarations for functions defined in gui.c */
void *pc_malloc(size_t size);
void pc_free_memory(void *memory);
char *pc_get_current_directory(void);
char **pc_enumerate_directory(struct fb_dir_entries *fb_cfg);
#include "input.h"

#define ICON_FOLDER "\xef\x81\xbb"  // Font Awesome folder
#define ICON_FILE   "\xef\x85\x9b"  // Font Awesome file
#define ICON_KEY    "\xef\x82\x84"  // Font Awesome key (FB_ENTRY_ACTION rows)
#define FB_DOUBLE_CLICK_MS 300

extern struct theme g_theme;

enum fb_focus_target {
    FB_FOCUS_LIST = 0,
    FB_FOCUS_UP,
    FB_FOCUS_CANCEL,
    FB_FOCUS_OK,
    FB_FOCUS_MAPPING
};

struct fb_dir_entries fb_cfg = {0};
static char box_text[1100] = {0};
static char save_filename[256] = {0};
static int show_drive_list = 0;
static struct modal_config *p_modal_cfg = 0;
static int g_selected = 0;
static int g_focused_control = FB_FOCUS_LIST;
static int fb_mapping = CART_MIN;
static int fb_win_w = 420;
static int fb_win_h = 380;
static int fb_last_selected = -1;
static unsigned int fb_last_click_time = 0;
static int fb_last_click_index = -1;
static struct key_status fb_key_left = {0};
static struct key_status fb_key_right = {0};


#ifdef _WIN32
    #if !defined(S_ISDIR)
        #define S_ISDIR(m) (((m) & _S_IFDIR) == _S_IFDIR)
    #endif
#endif

static int filebrowser_mapping_min(void)
{
    if (config_get_global_cfg()->general.machine == MACHINE_TYPE_5200)
        return CART_32K;
    return CART_PC_8K;
}

static int filebrowser_mapping_max(void)
{
    if (config_get_global_cfg()->general.machine == MACHINE_TYPE_5200)
        return CART_512K_HYBRID;
    return CART_BLIZZARD;
}

static int filebrowser_mapping_count(void)
{
    return filebrowser_mapping_max() - filebrowser_mapping_min() + 2;
}

static int filebrowser_mapping_to_position(int mapping)
{
    int min = filebrowser_mapping_min();
    int max = filebrowser_mapping_max();

    if (mapping >= min && mapping <= max)
        return mapping - min + 1;

    return 0;
}

static int filebrowser_position_to_mapping(int position)
{
    if (position <= 0)
        return CART_MIN;

    return filebrowser_mapping_min() + position - 1;
}

static void filebrowser_cycle_mapping(int delta)
{
    int count = filebrowser_mapping_count();
    int position = filebrowser_mapping_to_position(fb_mapping);

    position = (position + delta + count) % count;
    fb_mapping = filebrowser_position_to_mapping(position);
}

void filebrowser_free_list(void)
{
    if (fb_cfg.entry_path) {
        for (int i=0; i < fb_cfg.count; ++i) {
            pc_free_memory(fb_cfg.entry_path[i]);
        }
        pc_free_memory(fb_cfg.entry_path);
        fb_cfg.entry_path = NULL;
    }
    if (fb_cfg.entries) {
        for (int i=0; i < fb_cfg.count; ++i) {
            pc_free_memory(fb_cfg.entries[i]);
        }
        pc_free_memory(fb_cfg.entries);
        fb_cfg.entries = NULL;
        fb_cfg.count = 0;
    }
    if (fb_cfg.entry_type) {
        pc_free_memory(fb_cfg.entry_type);
        fb_cfg.entry_type = NULL;
    }
}

void filebrowser_free(void)
{
    if (fb_cfg.dir) {
        pc_free_memory(fb_cfg.dir);
        fb_cfg.dir = NULL;
    }
    filebrowser_free_list();
}

void filebrowser_exit(void) 
{
    filebrowser_free();
}

#ifdef __ANDROID__
/* Label shown for the row that opens Android's all-files-access prompt. */
#define FB_GRANT_ACCESS_LABEL "Grant Access to Whole SD Card..."

/* Access level the currently displayed roots list was built at, so
 * filebrowser_show can notice a grant that landed while we were backgrounded. */
static int fb_roots_access = -1;

static char *fb_strdup(const char *src)
{
    size_t len = strlen(src) + 1;
    char *dst = pc_malloc(len);

    if (dst)
        memcpy(dst, src, len);

    return dst;
}

/*
 * Android's equivalent of the Windows drive list.  Without it the browser could
 * only ever reach primary emulated storage, because scoped storage makes every
 * path between the app directory and a removable card unlistable - so a user
 * with ROMs on a microSD had no way to navigate to them.
 *
 * Roots come from pc_get_storage_root_* (app folders always, whole volumes once
 * access is granted).  When access is missing, a final action row offers the
 * prompt; if the platform has no such screen (common on TV boxes) the app
 * folders above it still work.
 */
static void filebrowser_list_android_roots(void)
{
    int roots = pc_get_storage_root_count();
    int has_access = pc_storage_has_full_access();
    int want_grant = !has_access;
    int total = roots + (want_grant ? 1 : 0);
    int i;

    fb_roots_access = has_access;

    filebrowser_free_list();

    if (total <= 0)
        return;

    fb_cfg.entries    = pc_malloc(sizeof(char *) * total);
    fb_cfg.entry_type = pc_malloc(sizeof(int) * total);
    fb_cfg.entry_path = pc_malloc(sizeof(char *) * total);
    if (!fb_cfg.entries || !fb_cfg.entry_type || !fb_cfg.entry_path) {
        filebrowser_free_list();
        return;
    }

    for (i = 0; i < roots; ++i) {
        fb_cfg.entries[fb_cfg.count]    = fb_strdup(pc_get_storage_root_label(i));
        fb_cfg.entry_path[fb_cfg.count] = fb_strdup(pc_get_storage_root_path(i));
        fb_cfg.entry_type[fb_cfg.count] = FB_ENTRY_DIR;
        if (!fb_cfg.entries[fb_cfg.count] || !fb_cfg.entry_path[fb_cfg.count]) {
            filebrowser_free_list();
            return;
        }
        fb_cfg.count++;
    }

    if (want_grant) {
        fb_cfg.entries[fb_cfg.count]    = fb_strdup(FB_GRANT_ACCESS_LABEL);
        fb_cfg.entry_path[fb_cfg.count] = fb_strdup("");
        fb_cfg.entry_type[fb_cfg.count] = FB_ENTRY_ACTION;
        if (!fb_cfg.entries[fb_cfg.count] || !fb_cfg.entry_path[fb_cfg.count]) {
            filebrowser_free_list();
            return;
        }
        fb_cfg.count++;
    }

    if (fb_cfg.dir)
        pc_free_memory(fb_cfg.dir);
    fb_cfg.dir = pc_malloc(1);
    if (fb_cfg.dir)
        fb_cfg.dir[0] = '\0';

    strcpy(box_text, "");
    show_drive_list = 1;
    g_selected = 0;
    fb_last_selected = -1;
    fb_last_click_time = 0;
    fb_last_click_index = -1;
}
#endif /* __ANDROID__ */

void filebrowser_list_drives(void)
{
#ifdef __ANDROID__
    filebrowser_list_android_roots();
    return;
#endif
#ifdef _WIN32
    char logical_drives[MAX_PATH] = { 0 };
    DWORD dwSize = sizeof(logical_drives);
    DWORD dwResult = GetLogicalDriveStringsA(dwSize, logical_drives);
    filebrowser_free_list();

    if (dwResult > 0 && dwResult <= MAX_PATH) {
        char *single_drive = logical_drives;

        while (*single_drive) {
            fb_cfg.count++;
            single_drive += strlen(single_drive) + 1;
        }

        fb_cfg.entries = pc_malloc(sizeof(char *) * fb_cfg.count);
        fb_cfg.entry_type = pc_malloc(sizeof(int) * fb_cfg.count);
        if (!fb_cfg.entries || !fb_cfg.entry_type) {
            filebrowser_free_list();
            return;
        }

        single_drive = logical_drives;
        for (int i = 0; i < fb_cfg.count; ++i) {
            fb_cfg.entries[i] = pc_malloc(strlen(single_drive) + 1);
            if (!fb_cfg.entries[i]) {
                filebrowser_free_list();
                return;
            }
            strcpy(fb_cfg.entries[i], single_drive);
            fb_cfg.entry_type[i] = FB_ENTRY_DIR;
            single_drive += strlen(single_drive) + 1;
        }
    }

    if (fb_cfg.dir)
        pc_free_memory(fb_cfg.dir);
    fb_cfg.dir = pc_malloc(1);
    if (fb_cfg.dir)
        fb_cfg.dir[0] = '\0';

    show_drive_list = 1;
    return;
#endif
}

void filebrowser_change_directory(char *dir)
{
    if (!dir || strlen(dir)==0) {
        filebrowser_list_drives();
        return;
    }

    if (fb_cfg.dir) {
        pc_free_memory(fb_cfg.dir);
    }

    fb_cfg.dir = malloc(strlen(dir)+1);
    strcpy(fb_cfg.dir, dir);

    //printf("%s\n", fb_cfg.dir);
    filebrowser_free_list();
    if (!pc_enumerate_directory(&fb_cfg)) {
#ifdef __ANDROID__
        /* Scoped storage refuses plenty of directories outright (/storage,
         * another app's Android/data, the whole card without all-files access).
         * Showing the resulting empty list is indistinguishable from an empty
         * folder, so fall back to the roots list - which always offers a
         * location that works.  NOTE: dir may alias box_text, which the roots
         * list clears, so nothing below may touch it. */
        filebrowser_list_drives();
        return;
#endif
    }

    //current_list = pc_glob_directory(current_dir, p_modal_cfg->modal_type==MODAL_FB_DIR ? "*/" : "*", &current_count);
    show_drive_list = 0;
    g_selected = 0;
    fb_last_selected = -1;
    fb_last_click_time = 0;
    fb_last_click_index = -1;

    /* Callers often pass box_text itself; avoid undefined self-overlapping strcpy. */
    if (box_text != dir)
        strcpy(box_text, dir);
}

void filebrowser_on_textbox_enter(void)
{
    char *index = 0;
    int file_is_dir = 0;
    struct stat file_status;
    int stat_error = stat(box_text, &file_status);
    if (!stat_error)
        file_is_dir = S_ISDIR(file_status.st_mode);
        //file_is_dir = (file_status.st_mode & S_IFDIR);

    if (p_modal_cfg->modal_type != MODAL_FB_DIR) {
        if (!stat_error && !file_is_dir) {
            p_modal_cfg->result = 1;
            p_modal_cfg->modal_active = 0;
            return;
        }
    }

    if (stat_error || !file_is_dir) {
        index = strrchr(box_text, '\\');
        if (!index) {
            index = strrchr(box_text, '/');
        }
	    if (index)
            *(index+1) = '\0';
    }

    stat_error = stat(box_text, &file_status);
    if (stat_error) {
        strcpy(box_text, fb_cfg.dir);
        return;
    }

    //printf("%s\n", box_text);
    filebrowser_change_directory(box_text);
}

void filebrowser_up(char *dir)
{
    char *index;
    char *index_up;

#ifdef __ANDROID__
    /* Stop at a storage root rather than walking into the unlistable parents
     * above it (.../Android/data, /storage/emulated/0 without permission,
     * /storage).  Going "up" from a root means picking a different volume. */
    if (dir) {
        size_t len = strlen(dir);
        int roots = pc_get_storage_root_count();
        int i;

        while (len > 1 && (dir[len-1] == '/' || dir[len-1] == '\\'))
            len--;

        if (len <= 1) {           /* already at "/" or empty */
            filebrowser_list_drives();
            return;
        }

        for (i = 0; i < roots; ++i) {
            const char *root = pc_get_storage_root_path(i);

            if (strlen(root) == len && !strncmp(dir, root, len)) {
                filebrowser_list_drives();
                return;
            }
        }
    }
#endif

    index = strrchr(dir, '\\');
    if (!index)
        index = strrchr(dir, '/');

    if (!index || index==dir || (*(index-1) == ':') )
        return;

    *index = '\0';

    //printf("dir: %x\n", dir);
    //printf("index: %x\n", index);
    index_up = strrchr(dir, '\\');
    if (!index_up)
        index_up = strrchr(dir, '/');

    //printf("index_up: %x\n", index_up);

    if (index_up) {
        *(index_up+1) = '\0';
    }

    strcpy(box_text, dir);

    filebrowser_change_directory(box_text);
}

static void filebrowser_reset_action_keys(void)
{
    fb_key_left.key_down = 0;
    fb_key_left.repeat_mode = 0;
    fb_key_left.last_trig_time = 0;
    fb_key_right.key_down = 0;
    fb_key_right.repeat_mode = 0;
    fb_key_right.last_trig_time = 0;
}

static int filebrowser_focus_count(void)
{
    if (p_modal_cfg && p_modal_cfg->modal_type == MODAL_FB_LOAD_ROM)
        return FB_FOCUS_MAPPING + 1;

    return FB_FOCUS_MAPPING;
}

static int filebrowser_build_path_from_name(const char *name,
                                            char *dst,
                                            size_t dst_size)
{
    const char *dir = fb_cfg.dir ? fb_cfg.dir : "";
    int dir_len;
    int needs_sep;
    int written;

    if (!name || !dst || dst_size == 0)
        return 0;

    dir_len = (int)strlen(dir);
    needs_sep = dir_len > 0 && dir[dir_len - 1] != '/' && dir[dir_len - 1] != '\\';
    written = snprintf(dst, dst_size, "%s%s%s", dir, needs_sep ? "/" : "", name);

    return written >= 0 && written < (int)dst_size;
}

static int filebrowser_build_entry_path(int index, char *dst, size_t dst_size)
{
    int written;

    if (index < 0 || index >= fb_cfg.count)
        return 0;

    /* Roots list: the row's label ("SD Card") is not its path, so take the
     * path carried alongside it rather than concatenating onto dir. */
    if (fb_cfg.entry_path && fb_cfg.entry_path[index]) {
        written = snprintf(dst, dst_size, "%s", fb_cfg.entry_path[index]);
        return written >= 0 && written < (int)dst_size;
    }

    return filebrowser_build_path_from_name(fb_cfg.entries[index], dst, dst_size);
}

static int filebrowser_finish_with_path(const char *path)
{
    int written;

    if (!path || !path[0])
        return 0;

    written = snprintf(box_text, sizeof(box_text), "%s", path);
    if (written < 0 || written >= (int)sizeof(box_text))
        return 0;

    p_modal_cfg->modal_active = 0;
    p_modal_cfg->result = 1;
    return 1;
}

static int filebrowser_finish_with_entry_path(int index)
{
    char path[sizeof(box_text)];

    if (!filebrowser_build_entry_path(index, path, sizeof(path)))
        return 0;

    return filebrowser_finish_with_path(path);
}

static int filebrowser_enter_entry_directory(int index)
{
    char path[sizeof(box_text)];

    if (index < 0 || index >= fb_cfg.count ||
        fb_cfg.entry_type[index] != FB_ENTRY_DIR)
        return 0;

    if (!filebrowser_build_entry_path(index, path, sizeof(path)))
        return 0;

    filebrowser_change_directory(path);
    return 1;
}

static int filebrowser_run_entry_action(int index);

static int filebrowser_select_current_directory(void)
{
    if (fb_cfg.dir && fb_cfg.dir[0])
        return filebrowser_finish_with_path(fb_cfg.dir);

    if (show_drive_list && g_selected >= 0 && g_selected < fb_cfg.count) {
        /* Ok on the roots list acts on the highlighted row: pick that volume,
         * or run the row's action so Ok and Enter behave the same. */
        if (fb_cfg.entry_type[g_selected] == FB_ENTRY_ACTION)
            return filebrowser_run_entry_action(g_selected);

        if (fb_cfg.entry_type[g_selected] == FB_ENTRY_DIR)
            return filebrowser_finish_with_entry_path(g_selected);
    }

    return 0;
}

static int filebrowser_accept_save_filename(void)
{
    char path[sizeof(box_text)];

    if (!save_filename[0])
        return 0;

    if (!filebrowser_build_path_from_name(save_filename, path, sizeof(path)))
        return 0;

    return filebrowser_finish_with_path(path);
}

/* Roots-list rows that do something instead of navigating.  Currently only the
 * Android all-files-access prompt. */
static int filebrowser_run_entry_action(int index)
{
    if (index < 0 || index >= fb_cfg.count ||
        fb_cfg.entry_type[index] != FB_ENTRY_ACTION)
        return 0;

#ifdef __ANDROID__
    /* Fire-and-forget: on API 30+ this hands off to a Settings screen, so the
     * grant arrives while we are backgrounded.  filebrowser_show polls for it
     * and rebuilds the list on return. */
    pc_storage_request_full_access();
#endif

    return 1;
}

static int filebrowser_activate_selected_entry(int select_current_if_empty)
{
    char path[sizeof(box_text)];

    if (g_selected < 0 || g_selected >= fb_cfg.count) {
        if (select_current_if_empty && fb_cfg.fb_type == MODAL_FB_DIR)
            return filebrowser_select_current_directory();
        return 0;
    }

    if (fb_cfg.entry_type[g_selected] == FB_ENTRY_ACTION)
        return filebrowser_run_entry_action(g_selected);

    if (fb_cfg.entry_type[g_selected] == FB_ENTRY_DIR)
        return filebrowser_enter_entry_directory(g_selected);

    if (!filebrowser_build_entry_path(g_selected, path, sizeof(path)))
        return 0;

    if (p_modal_cfg->modal_type == MODAL_FB_SAVE) {
        strncpy(save_filename, fb_cfg.entries[g_selected], sizeof(save_filename) - 1);
        save_filename[sizeof(save_filename) - 1] = '\0';
        return 1;
    }

    if (fb_cfg.fb_type != MODAL_FB_DIR)
        return filebrowser_finish_with_path(path);

    return 0;
}

static int filebrowser_activate_ok(void)
{
    if (p_modal_cfg->modal_type == MODAL_FB_SAVE)
        return filebrowser_accept_save_filename();

    if (fb_cfg.fb_type == MODAL_FB_DIR)
        return filebrowser_select_current_directory();

    return filebrowser_activate_selected_entry(0);
}

void filebrowser_init(struct modal_config *cfg)
{
    filebrowser_free();
    config_load_filebrowser_size(&fb_win_w, &fb_win_h);
    const char *empty = "";
    strcpy(box_text, "");
    const char *dir = (const char *)cfg->data;
    int type = cfg->modal_type;
    g_selected = 0;
    g_focused_control = FB_FOCUS_LIST;
    fb_mapping = CART_MIN;
    fb_last_selected = -1;
    fb_last_click_time = 0;
    fb_last_click_index = -1;
    filebrowser_reset_action_keys();

    p_modal_cfg = cfg;
    fb_cfg.fb_type = type;
    save_filename[0] = '\0';

    if (!dir)
        dir = empty;

    /*
     * Check if this is a relative directory
     */
    if ( !dir || ((dir[0] != '/') && (dir[1] != ':')) ) {
        char *cwd = NULL;
#ifdef __ANDROID__
        //cwd = (char *)malloc ( sizeof(char)*1100 );
        cwd = (char *)pc_malloc(1100);
        pc_get_external_sdcard_path(cwd);   /* writes the app SD-card path */
        strcat (cwd, "/");
#else
        cwd = pc_get_current_directory();
#endif
        fb_cfg.dir = malloc(strlen(dir)+strlen(cwd)+1);
        sprintf(fb_cfg.dir, "%s%s", cwd, dir);
        pc_free_memory(cwd);
    }
    else {
        fb_cfg.dir = malloc(strlen(dir)+1);
        strcpy(fb_cfg.dir, dir);
    }

    /*
     * Check if directory is a file,
     *   and if so truncate it
     */
    char *index = 0;
    int file_is_dir = 0;
    struct stat file_status;
    int stat_error = stat(fb_cfg.dir, &file_status);
    if (!stat_error)
        file_is_dir = S_ISDIR(file_status.st_mode);
        //file_is_dir = (file_status.st_mode & S_IFDIR);

    if (stat_error || !file_is_dir) {
        index = strrchr(fb_cfg.dir, '/');
        if (!index) index = strrchr(fb_cfg.dir, '\\');
        if (index && *(index+1) && type == MODAL_FB_SAVE)
            strncpy(save_filename, index+1, sizeof(save_filename)-1);
        index = strrchr(fb_cfg.dir, '\\');
        if (!index) {
            index = strrchr(fb_cfg.dir, '/');
        }
	    if (index)
            *(index+1) = '\0';
    }

    /* If the resolved directory still doesn't exist, fall back to CWD */
    {
        struct stat check_status;
        if (stat(fb_cfg.dir, &check_status) != 0 || !S_ISDIR(check_status.st_mode)) {
            free(fb_cfg.dir);
#ifdef __ANDROID__
            /* CWD is "/" here and is not listable under scoped storage, so it
             * is useless as a fallback - the roots list is not. */
            fb_cfg.dir = NULL;
            filebrowser_list_drives();
            return;
#else
            fb_cfg.dir = pc_get_current_directory();
#endif
        }
    }

    strcpy(box_text, fb_cfg.dir);

    if (!pc_enumerate_directory(&fb_cfg)) {
#ifdef __ANDROID__
        /* Saved config may point at a card that has since been removed, or at a
         * path that needs access we no longer hold. */
        filebrowser_list_drives();
        return;
#endif
    }
    //current_list = pc_glob_directory(current_dir, type==MODAL_FB_DIR ? "*/" : "*", &current_count);
}

char * filebrowser_result(void)
{
    return box_text;
}

int filebrowser_get_mapping(void)
{
    return fb_mapping;
}

/* Push the yellow selection theme onto the selectable backgrounds for the
 * file list: only the selected row highlights (selection_bg); hover/press on
 * non-selected rows show no highlight. Pop in strict reverse order. */
static void filebrowser_push_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);
}

static void filebrowser_pop_list_selectable_style(struct nk_context *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);
}

void filebrowser_show(struct nk_context *ctx,
                          int screen_w, int screen_h)
{
    static struct nk_list_view list;
    const int ITEM_HEIGHT = 28;
    static int path_edit_active = 0;
    int is_selected;
    int clicked = 0;
    struct nk_color file_color       = g_theme.fb_file_color;
    static const char *button_labels[FB_FOCUS_OK + 1] = {
        "", "Up", "Cancel", "Ok"
    };
    float mapping_row_h;

    struct nk_color folder_color       = g_theme.fb_folder_color;


    ctx->style.font = &(g_theme.font_normal->handle);
    theme_update_dimensions(ctx);
    mapping_row_h = g_theme.row_height * 1.2f;

#ifdef __ANDROID__
    /* The all-files-access grant happens on a Settings screen, so it lands
     * while the app is backgrounded and there is no event to hang this off.
     * Poll on a timer (not every frame - each check is a JNI call) so the roots
     * list gains its whole-volume entries once the user comes back. */
    if (show_drive_list) {
        static unsigned int last_access_check = 0;
        unsigned int now = pc_get_ticks();

        if (now - last_access_check > 500) {
            last_access_check = now;
            if (pc_storage_has_full_access() != fb_roots_access)
                filebrowser_list_drives();
        }
    }
#endif

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

        s->background = g_theme.modal_overlay;

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

        nk_end(ctx);

        s->background = old_bg;
    }

    /* ---------------------------------------------------------
     * 2) Modal window (no scrollbar)
     * --------------------------------------------------------- */
    struct nk_rect modal =
        nk_rect((screen_w - fb_win_w) / 2,
                (screen_h - fb_win_h) / 2,
                fb_win_w, fb_win_h);

    if (nk_begin(ctx, p_modal_cfg->title,
    //if (nk_begin(ctx, "File Browser##modal",
        modal,
        NK_WINDOW_BORDER |
        NK_WINDOW_TITLE |
        NK_WINDOW_MOVABLE |
        NK_WINDOW_SCALABLE |
        NK_WINDOW_NO_SCROLLBAR))
    {
        /* This is a modal: always the focused, top-most window.  When it is
           opened by a mouse click the menu's click latches NK_WINDOW_ROM onto
           this window on its creation frame, and because nk_window_set_focus()
           below pins us to ctx->end every frame, Nuklear's ROM-clear path can
           never run -- so ROM stays stuck and every widget ignores the mouse
           (keyboard nav still works as it reads ctx->input directly).  Force
           ROM off here, before any widgets, so the mouse always works. */
        ctx->current->flags &= ~(nk_flags)NK_WINDOW_ROM;
        ctx->current->layout->flags &= ~(nk_flags)NK_WINDOW_ROM;

        struct nk_command_buffer *canvas = nk_window_get_canvas(ctx);
        int enter_hit = input_check_select_item(ctx);

        /* Save window size whenever it changes */
        {
            struct nk_rect cur = nk_window_get_bounds(ctx);
            int cur_w = (int)cur.w;
            int cur_h = (int)cur.h;
            if (cur_w != fb_win_w || cur_h != fb_win_h) {
                fb_win_w = cur_w;
                fb_win_h = cur_h;
                config_save_filebrowser_size(fb_win_w, fb_win_h);
            }
        }

        int old_selected = g_selected;
        int keyboard_nav_updated = 0;
        int key_updated = input_check_vertical_navigation_ex(ctx, &g_selected, fb_cfg.count,
                                                             list.count > 1 ? list.count - 1 : 1,
                                                             &keyboard_nav_updated);
        int selection_moved = (old_selected != g_selected);
        if (selection_moved)
            key_updated = 1;

        if (!path_edit_active) {
            char typeahead[64];
            if (input_check_typeahead(ctx, typeahead, sizeof(typeahead), 1000) && typeahead[0]) {
                int tlen = (int)strlen(typeahead);
                /* Files are sorted after all directories — skip past the dir section */
                int first_file = 0;
                while (first_file < fb_cfg.count && fb_cfg.entry_type[first_file] == FB_ENTRY_DIR)
                    first_file++;
                /* Lower-bound binary search within the files section */
                int lo = first_file, hi = fb_cfg.count;
                while (lo < hi) {
                    int mid = (lo + hi) / 2;
                    if (strncasecmp(fb_cfg.entries[mid], typeahead, tlen) < 0)
                        lo = mid + 1;
                    else
                        hi = mid;
                }
                if (lo < fb_cfg.count && fb_cfg.entry_type[lo] == FB_ENTRY_FILE &&
                    strncasecmp(fb_cfg.entries[lo], typeahead, tlen) == 0) {
                    g_selected = lo;
                    key_updated = 1;
                    selection_moved = 1;
                    keyboard_nav_updated = 1;
                }
            }
        }

        int focus_count = filebrowser_focus_count();
        if (g_focused_control < FB_FOCUS_LIST || g_focused_control >= focus_count)
            g_focused_control = FB_FOCUS_LIST;
        int tab_changed = input_check_horizontal_navigation(ctx, &g_focused_control, focus_count);

        if (keyboard_nav_updated && selection_moved && !tab_changed)
            g_focused_control = FB_FOCUS_LIST;

        //ctx->active = ctx->current;
        nk_window_set_focus(ctx, p_modal_cfg->title);

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

        /* Title / instructions */
        float content_w = nk_window_get_content_region_size(ctx).x;
        nk_layout_row(ctx, NK_STATIC, g_theme.row_height+8, 3, (float[]){content_w-70, 5, 50});
        //nk_layout_row_dynamic(ctx, 30, 1);
        //nk_label(ctx, "Select an item from the list", NK_TEXT_LEFT);
        nk_flags res = nk_edit_string_zero_terminated(ctx,
            NK_EDIT_FIELD | NK_EDIT_SIG_ENTER,
            box_text,
            sizeof(box_text),
            nk_filter_default);
        path_edit_active = (res & NK_EDIT_ACTIVE) ? 1 : 0;

        if (res & NK_EDIT_ACTIVE) {
            if (tab_changed || key_updated) {
                if (ctx->current)
                    ctx->current->edit.active = 0;
            }
            else {
                if (res & NK_EDIT_COMMITED) {
                    //printf("Enter on TextBox");
                    filebrowser_on_textbox_enter();
                    enter_hit = 0;
                }
            }
        }

        if (!path_edit_active && g_focused_control == FB_FOCUS_LIST) {
            int handled_list_action = 0;

            if (input_check_left_action(ctx, &fb_key_left)) {
                filebrowser_up(fb_cfg.dir);
                handled_list_action = 1;
            }
            else if (input_check_right_action(ctx, &fb_key_right)) {
                handled_list_action = filebrowser_enter_entry_directory(g_selected);
            }

            if (enter_hit && !handled_list_action) {
                filebrowser_activate_selected_entry(1);
                enter_hit = 0;
            }
        }
        else {
            filebrowser_reset_action_keys();
        }

        nk_spacing(ctx, 1);

        if (g_focused_control == FB_FOCUS_UP)
            theme_push_selected_button_color(ctx);

        struct nk_rect bounds = nk_widget_bounds(ctx);

        if (nk_button_label(ctx, button_labels[FB_FOCUS_UP]) ||
            (g_focused_control == FB_FOCUS_UP && enter_hit)) {
            filebrowser_up(fb_cfg.dir);
        }

        // Highlight the focused button
        if (g_focused_control == FB_FOCUS_UP) {
            nk_stroke_rect(canvas, bounds, 4.0f, 2.0f, g_theme.selection);
            theme_pop_selected_button_color(ctx);
        }

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

        /* -----------------------------------------------------
         * 3) Scrollable list using nk_list_view
         * ----------------------------------------------------- */

        /* Derive list height from available content area minus fixed-height rows.
         * Each nk_spacing(ctx,1) on a 1-col row triggers nk_panel_alloc_row,
         * creating a phantom row (double-advance). We have 3 such spacers, each
         * adding an extra (3 + spacing.y) to vertical consumption. */
        float content_h = nk_window_get_content_region_size(ctx).y;
        float fixed_h = 56.0f + g_theme.row_height
                        + 9.0f * ctx->style.window.spacing.y;
        if (p_modal_cfg->modal_type == MODAL_FB_SAVE)
            fixed_h += g_theme.row_height_1_5 + 6.0f + 3.0f * ctx->style.window.spacing.y;
        if (p_modal_cfg->modal_type == MODAL_FB_LOAD_ROM)
            fixed_h += mapping_row_h + 6.0f + 3.0f * ctx->style.window.spacing.y;
        float list_h = content_h - fixed_h;
        if (list_h < ITEM_HEIGHT * 3) list_h = ITEM_HEIGHT * 3;

        int actual_height = ITEM_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_entry_count = fb_cfg.count;
        if (fb_cfg.count > fully_visible_rows)
            virtual_entry_count += (fully_visible_rows >= 4) ? 2 : 1;

        nk_layout_row_dynamic(ctx, (int)list_h, 1);
        struct nk_rect list_bounds = nk_widget_bounds(ctx);

        nk_list_view_begin(ctx, &list,
                           "__modal_list",
                           NK_WINDOW_BORDER,
                           ITEM_HEIGHT,
                           virtual_entry_count);

        /* 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 (g_selected != fb_last_selected && list.count > 0) {
            if (g_selected <= list.begin + 1) {
                int target = g_selected - 2;
                if (target < 0) target = 0;
                list.scroll_value = target * actual_height;
            } else if (g_selected >= list.end - 2) {
                int target = g_selected - list.count + 3;
                if (target < 0) target = 0;
                list.scroll_value = target * actual_height;
            }

            if ((int)list.scroll_value < 0)
                list.scroll_value = 0;
            int max_scroll = (virtual_entry_count - list.count) * actual_height;
            if (max_scroll < 0) max_scroll = 0;
            if ((int)list.scroll_value > max_scroll)
                list.scroll_value = max_scroll;

            list.begin = (int)(list.scroll_value / actual_height);
            list.end = list.begin + list.count;
            if (list.end > virtual_entry_count) list.end = virtual_entry_count;
        }
        fb_last_selected = g_selected;

        //nk_layout_row_static(ctx, ITEM_HEIGHT, 200, 1);
        float width = nk_window_get_content_region_size(ctx).x;

        float cols[] = {
            40,               // icon
            width - 40        // text
        };

        nk_layout_row(ctx, NK_STATIC, ITEM_HEIGHT, 2, cols);

        filebrowser_push_list_selectable_style(ctx);

        //nk_layout_row_begin(ctx, NK_STATIC, ITEM_HEIGHT, 2);
        //nk_layout_row_push(ctx, 40);             // icon column
        //nk_layout_row_push(ctx, width - 40);     // text column
        //nk_layout_row_end(ctx);

        for (int i = list.begin; i < list.end && i < fb_cfg.count; ++i) {
            //char buf[32];
            //snprintf(buf, sizeof(buf), "Item %d", i);

            //if (first_run == 1) {
            //    printf("index: %d; Label: %s\n", i, fb_cfg.entries[i]);
            //}
            //nk_label(ctx, current_list[i], NK_TEXT_LEFT);
            is_selected = (i == g_selected);

            int entry_is_dir = (fb_cfg.entry_type[i] == FB_ENTRY_DIR);
            int entry_is_action = (fb_cfg.entry_type[i] == FB_ENTRY_ACTION);
            /* Action rows borrow the folder colour so they read as navigation
             * chrome rather than as a selectable ROM. */
            struct nk_color *entry_color = (entry_is_dir || entry_is_action) ? &folder_color : &file_color;
            const char *icon = entry_is_action ? ICON_KEY
                                               : (entry_is_dir ? ICON_FOLDER : ICON_FILE);
            clicked = 0;

            struct nk_vec2 old_spacing = ctx->style.window.spacing;
            ctx->style.window.spacing.x = 0;

            /* Color only the icon glyph (yellow folder / default file) for all
             * states; the filename label below renders in the default text
             * color. */
            nk_style_push_color(ctx, &ctx->style.selectable.text_normal, *entry_color);
            nk_style_push_color(ctx, &ctx->style.selectable.text_normal_active, *entry_color);
            nk_style_push_color(ctx, &ctx->style.selectable.text_hover, *entry_color);
            nk_style_push_color(ctx, &ctx->style.selectable.text_hover_active, *entry_color);
            nk_style_push_color(ctx, &ctx->style.selectable.text_pressed, *entry_color);
            nk_style_push_color(ctx, &ctx->style.selectable.text_pressed_active, *entry_color);
            clicked |= nk_selectable_label(ctx, icon, NK_TEXT_CENTERED, &is_selected);
            nk_style_pop_color(ctx);
            nk_style_pop_color(ctx);
            nk_style_pop_color(ctx);
            nk_style_pop_color(ctx);
            nk_style_pop_color(ctx);
            nk_style_pop_color(ctx);

            clicked |= nk_selectable_label(ctx, fb_cfg.entries[i], NK_TEXT_LEFT, &is_selected);

            ctx->style.window.spacing = old_spacing;

            if (clicked) {
                unsigned int now = pc_get_ticks();
                unsigned int elapsed = now - fb_last_click_time;
                if (g_selected == i && i == fb_last_click_index &&
                    elapsed < FB_DOUBLE_CLICK_MS) {
                    if (fb_cfg.entry_type[i] == FB_ENTRY_DIR) {
                        filebrowser_enter_entry_directory(i);
                        break;
                    }
                    else {
                        filebrowser_activate_selected_entry(0);
                    }
                }
                g_selected = i;
                g_focused_control = FB_FOCUS_LIST;
                fb_last_selected = i;
                fb_last_click_index = i;
                fb_last_click_time = now;
                if (p_modal_cfg->modal_type == MODAL_FB_SAVE && fb_cfg.entry_type[i] == FB_ENTRY_FILE) {
                    strncpy(save_filename, fb_cfg.entries[i], sizeof(save_filename)-1);
                    save_filename[sizeof(save_filename)-1] = '\0';
                }
            }
        }

        filebrowser_pop_list_selectable_style(ctx);

        nk_list_view_end(&list);
        if (g_focused_control == FB_FOCUS_LIST)
            nk_stroke_rect(canvas, list_bounds, 4.0f, 2.0f, g_theme.selection);

        /* -----------------------------------------------------
         * 4) Save filename edit box (MODAL_FB_SAVE only)
         * ----------------------------------------------------- */
        if (p_modal_cfg->modal_type == MODAL_FB_SAVE) {
            nk_layout_row_dynamic(ctx, 3, 1);
            nk_spacing(ctx, 1);
            nk_layout_row_dynamic(ctx, (int)g_theme.row_height_1_5, 1);
            nk_edit_string_zero_terminated(ctx,
                NK_EDIT_FIELD,
                save_filename,
                sizeof(save_filename),
                nk_filter_default);
        }

        if (p_modal_cfg->modal_type == MODAL_FB_LOAD_ROM) {
            char mapping_text[64];
            int mapping_focused = (g_focused_control == FB_FOCUS_MAPPING);

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

            if (mapping_focused) {
                int mapping_position = filebrowser_mapping_to_position(fb_mapping);
                if (input_check_value_navigation(ctx, &mapping_position, 0,
                            filebrowser_mapping_count() - 1))
                    fb_mapping = filebrowser_position_to_mapping(mapping_position);
                if (enter_hit)
                    filebrowser_cycle_mapping(1);
            }

            if (mapping_focused)
                theme_push_selected_button_color(ctx);
            widget_push_select_button_style(ctx);

            float mapping_content_w = nk_window_get_content_region_size(ctx).x;
            float mapping_label_w = 92.0f;
            float mapping_arrow_w = mapping_row_h;
            float mapping_value_w = 180.0f;
            float mapping_value_max = mapping_content_w - mapping_label_w
                                      - (2.0f * mapping_arrow_w)
                                      - (3.0f * ctx->style.window.spacing.x);
            if (mapping_value_max < 40.0f)
                mapping_value_max = 40.0f;
            if (mapping_value_w > mapping_value_max)
                mapping_value_w = mapping_value_max;

            nk_layout_row(ctx, NK_STATIC, mapping_row_h, 4,
                    (float[]){mapping_label_w, mapping_arrow_w,
                              mapping_value_w, mapping_arrow_w});

            nk_label(ctx, "Mapping:    ", NK_TEXT_LEFT);
            if (nk_button_symbol(ctx, NK_SYMBOL_TRIANGLE_LEFT))
                filebrowser_cycle_mapping(-1);
            cart_get_mapping_string(fb_mapping, mapping_text);
            nk_label(ctx, mapping_text, NK_TEXT_CENTERED);
            if (nk_button_symbol(ctx, NK_SYMBOL_TRIANGLE_RIGHT))
                filebrowser_cycle_mapping(1);

            widget_pop_select_button_style(ctx);
            if (mapping_focused)
                theme_pop_selected_button_color(ctx);
        }

        /* -----------------------------------------------------
         * 5) Buttons
         * ----------------------------------------------------- */
        nk_layout_row_dynamic(ctx, 3, 1);
        nk_spacing(ctx, 1);

        //nk_layout_row(ctx, NK_STATIC, g_theme.row_height, 3, (float[]){window_w/3, window_w/3, window_w/3});
        nk_layout_row_dynamic(ctx, 30, 4);
        nk_spacing(ctx, 1);
        nk_spacing(ctx, 1);

        for (int i = FB_FOCUS_CANCEL; i <= FB_FOCUS_OK; ++i) {

            bounds = nk_widget_bounds(ctx);

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

            if (nk_button_label(ctx, button_labels[i]) ||
                (g_focused_control == i && enter_hit)) {
                switch(i) {
                    case FB_FOCUS_CANCEL:
                        p_modal_cfg->modal_active = 0;
                        p_modal_cfg->result = 0;
                        break;

                    case FB_FOCUS_OK:
                        filebrowser_activate_ok();
                        break;
                }
            }

            // Highlight the focused button
            if (g_focused_control == i) {
                nk_stroke_rect(canvas, bounds, 4.0f, 2.0f, g_theme.selection);
                theme_pop_selected_button_color(ctx);
            }

        } // end for buttons
    }
    nk_end(ctx);
}
