/******************************************************************************
*
* Copyright (C) 2006-2025  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: gui.c
*
* DESCRIPTION:  Nuklear-based GUI bridge for kat5200. Adapts katgui to work
*               with kat5200's main loop and configuration system.
*
* AUTHORS:  bberlin
*
* CHANGE LOG
*
*  VER     DATE     CHANGED BY   DESCRIPTION OF CHANGE
* ------  --------  ----------   ------------------------------------
* 0.9.1   01/2026   bberlin      Creation - replaced guichan with Nuklear
******************************************************************************/

#include <SDL3/SDL.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_filesystem.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <math.h>
#include <assert.h>
#include <limits.h>
#include <time.h>

#include "../kat5200.h"
#include "../interface/sdl_if.h"
#include "../interface/katconfig.h"
#include "../interface/util.h"
#include "gui.h"
#include "modal.h"

#define NK_INCLUDE_STANDARD_VARARGS
#define NK_INCLUDE_STANDARD_IO
#define NK_INCLUDE_FONT_BAKING
#define NK_INCLUDE_DEFAULT_FONT

#define NK_INCLUDE_COMMAND_USERDATA
#define NK_INCLUDE_VERTEX_BUFFER_OUTPUT

#ifndef NK_INCLUDE_FIXED_TYPES
    #define NK_INT8              Sint8
    #define NK_UINT8             Uint8
    #define NK_INT16             Sint16
    #define NK_UINT16            Uint16
    #define NK_INT32             Sint32
    #define NK_UINT32            Uint32
    #define NK_SIZE_TYPE         uintptr_t
    #define NK_POINTER_TYPE      uintptr_t
#endif

#define NK_ASSERT(condition)      SDL_assert(condition)
#define NK_STATIC_ASSERT(exp)     SDL_COMPILE_TIME_ASSERT(, exp)
#define NK_MEMSET(dst, c, len)    SDL_memset(dst, c, len)
#define NK_MEMCPY(dst, src, len)  SDL_memcpy(dst, src, len)
#define NK_VSNPRINTF(s, n, f, a)  SDL_vsnprintf(s, n, f, a)
#define NK_STRTOD(str, endptr)    SDL_strtod(str, endptr)

#define NK_INV_SQRT(f)            (1.0f / SDL_sqrtf(f))
#define NK_SIN(f)                 SDL_sinf(f)
#define NK_COS(f)                 SDL_cosf(f)

#define STBTT_ifloor(x)       ((int)SDL_floor(x))
#define STBTT_iceil(x)        ((int)SDL_ceil(x))
#define STBTT_sqrt(x)         SDL_sqrt(x)
#define STBTT_pow(x,y)        SDL_pow(x,y)
#define STBTT_fmod(x,y)       SDL_fmod(x,y)
#define STBTT_cos(x)          SDL_cosf(x)
#define STBTT_acos(x)         SDL_acos(x)
#define STBTT_fabs(x)         SDL_fabs(x)
#define STBTT_assert(x)       SDL_assert(x)
#define STBTT_strlen(x)       SDL_strlen(x)
#define STBTT_memcpy          SDL_memcpy
#define STBTT_memset          SDL_memset
#define stbtt_uint8           Uint8
#define stbtt_int8            Sint8
#define stbtt_uint16          Uint16
#define stbtt_int16           Sint16
#define stbtt_uint32          Uint32
#define stbtt_int32           Sint32
#define STBRP_SORT            SDL_qsort
#define STBRP_ASSERT          SDL_assert

#define NK_IMPLEMENTATION
#include "theme.h"
#include "filebrowser.h"
#include "guitop.h"
#include "font_inter_semibold.h"
//#include "font_erasdemi.h"
#include "font_sf_atarian_system.h"
#include "font_fa.h"
#include "font_fa_solid.h"
#define NK_SDL3_RENDERER_IMPLEMENTATION
#include "nuklear_sdl3_renderer.h"

/* External GUI theme */
extern struct theme g_theme;
void menu_options_setup_exit(void);

/* Forward declarations for input capture (defined in menu_input.c) */
int menu_input_capture_is_active(void);
int menu_input_capture_process_event(SDL_Event *event);

#define MAX_EVENTS 64

/* GUI state */
static struct nk_context *ctx = NULL;
static SDL_Window *win = NULL;
static SDL_Renderer *renderer = NULL;
static int gui_initialized = 0;
static int gui_running = 0;
static int gui_exit_requested = 0;
static int gui_launch_requested = 0;
static int gui_back_requested = 0;
static int gui_no_reset_requested = 0;
static int gui_can_go_back_flag = 0;
static int gui_debug_mode = 0;
static int gui_run_current = 0;
static int window_w = 0;          /* logical canvas size = pixels / g_ui_scale */
static int window_h = 0;
static int render_output_w = 1;   /* full renderer output, before safe-area clipping */
static int render_output_h = 1;
static SDL_Rect gui_render_area = {0, 0, 1, 1}; /* safe area in logical coordinates */
static float g_ui_scale = 1.0f;   /* DPI/content scale; magnifies the whole GUI */
static int gui_rebuild_requested = 0;
#define GUI_MAX_SCALE 2.25f       /* cap so portrait layout width isn't clipped */
#define GUI_TV_MIN_SCALE 1.6f     /* 10-foot UI floor: TVs report ~1.0 displayScale */
static int gui_first_run_prompt_pending = 0;

#ifdef __ANDROID__
/* Single-finger touch handling.  Touch->mouse synthesis is disabled (see
 * pc_init) because GUI widgets trigger on mouse-press, so a synthetic touch-down
 * would activate whatever a swipe starts on.  Instead: a drag scrolls, and a tap
 * (finger up with no movement) emits a click.  GUI is single-touch. */
static SDL_FingerID g_touch_finger = 0;
static int   g_touch_active = 0;
static float g_touch_down_x = 0.0f;   /* gesture start, for slop + axis lock */
static float g_touch_down_y = 0.0f;
static float g_touch_last_x = 0.0f;   /* previous motion sample (incremental delta) */
static float g_touch_last_y = 0.0f;
static int   g_touch_moved  = 0;      /* crossed slop -> treat as scroll     */
static int   g_touch_axis   = 0;      /* 0 undecided, 1 vertical, 2 horizontal */
static int   g_tap_release_pending = 0;
static float g_tap_release_x = 0.0f;
static float g_tap_release_y = 0.0f;
#define GUI_TAP_SLOP 8.0f             /* logical px of drag before it's a scroll */
#define GUI_AXIS_V 1
#define GUI_AXIS_H 2
#endif

/* Event queue */
static SDL_Event event_queue[MAX_EVENTS];
static int event_count = 0;

/* Forward declarations */
void guitop(struct nk_context *ctx, int window_w, int window_h);
void guitop_esc(void);

static void gui_handle_joystick_nav(struct nk_context *ctx, SDL_Event *evt)
{
    /* Trigger threshold: ~25% of full range (32767). */
    #define TRIGGER_THRESHOLD 8000
    static int left_trigger_down = 0;
    static int right_trigger_down = 0;

    switch (evt->type) {
        case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
            if (evt->gaxis.axis == SDL_GAMEPAD_AXIS_LEFT_TRIGGER) {
                int now_down = evt->gaxis.value > TRIGGER_THRESHOLD;
                if (now_down != left_trigger_down) {
                    left_trigger_down = now_down;
                    nk_input_key(ctx, NK_KEY_SCROLL_UP, now_down ? nk_true : nk_false);
                }
            } else if (evt->gaxis.axis == SDL_GAMEPAD_AXIS_RIGHT_TRIGGER) {
                int now_down = evt->gaxis.value > TRIGGER_THRESHOLD;
                if (now_down != right_trigger_down) {
                    right_trigger_down = now_down;
                    nk_input_key(ctx, NK_KEY_SCROLL_DOWN, now_down ? nk_true : nk_false);
                }
            }
            break;
        }
        case SDL_EVENT_JOYSTICK_HAT_MOTION:
            if (evt->jhat.hat != 0) break;
            nk_input_key(ctx, NK_KEY_UP,    (evt->jhat.value & SDL_HAT_UP)    ? nk_true : nk_false);
            nk_input_key(ctx, NK_KEY_DOWN,  (evt->jhat.value & SDL_HAT_DOWN)  ? nk_true : nk_false);
            nk_input_key(ctx, NK_KEY_LEFT,  (evt->jhat.value & SDL_HAT_LEFT)  ? nk_true : nk_false);
            nk_input_key(ctx, NK_KEY_RIGHT, (evt->jhat.value & SDL_HAT_RIGHT) ? nk_true : nk_false);
            break;
        case SDL_EVENT_JOYSTICK_BUTTON_DOWN:
        case SDL_EVENT_JOYSTICK_BUTTON_UP: {
            nk_bool down = (evt->type == SDL_EVENT_JOYSTICK_BUTTON_DOWN) ? nk_true : nk_false;
            switch (evt->jbutton.button) {
                case 0: nk_input_key(ctx, NK_KEY_ENTER, down); break;
                case 1: if (down) guitop_esc(); break;
                /* Buttons 4/5 are left/right shoulder on most modern controllers */
                case 4: nk_input_key(ctx, NK_KEY_SHIFT, down);
                        nk_input_key(ctx, NK_KEY_TAB,   down); break;
                case 5: nk_input_key(ctx, NK_KEY_TAB, down); break;
                default: break;
            }
            break;
        }
        case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
        case SDL_EVENT_GAMEPAD_BUTTON_UP: {
            nk_bool down = (evt->type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) ? nk_true : nk_false;
            switch (evt->gbutton.button) {
                case SDL_GAMEPAD_BUTTON_DPAD_UP:       nk_input_key(ctx, NK_KEY_UP,    down); break;
                case SDL_GAMEPAD_BUTTON_DPAD_DOWN:     nk_input_key(ctx, NK_KEY_DOWN,  down); break;
                case SDL_GAMEPAD_BUTTON_DPAD_LEFT:     nk_input_key(ctx, NK_KEY_LEFT,  down); break;
                case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:    nk_input_key(ctx, NK_KEY_RIGHT, down); break;
                case SDL_GAMEPAD_BUTTON_SOUTH:         nk_input_key(ctx, NK_KEY_ENTER, down); break;
                case SDL_GAMEPAD_BUTTON_EAST:          if (down) guitop_esc(); break;
                case SDL_GAMEPAD_BUTTON_LEFT_SHOULDER:  nk_input_key(ctx, NK_KEY_SHIFT, down);
                                                        nk_input_key(ctx, NK_KEY_TAB,   down); break;
                case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: nk_input_key(ctx, NK_KEY_TAB, down); break;
                default: break;
            }
            break;
        }
        default: break;
    }
}
void message_init(struct modal_config *p_cfg);
void debugger_init(void);
void debugger_show(void);
int debugger_is_active(void);
int debugger_wants_input(void);

int gui_try_open_debugger(void)
{
    if (!pc_debugger_available()) {
        guitop_show_toast_msg("Connect a mouse to use the debugger.");
        return 0;
    }

    debugger_init();
    debugger_show();
    gui_running = 1;
    return 1;
}

void gui_open_debugger(void)
{
    /* Retained for older call sites; new UI paths use gui_try_open_debugger(). */
    (void)gui_try_open_debugger();
}

static void cpu_crash_post_show(struct modal_config *cfg)
{
    if (cfg->result)
        (void)gui_try_open_debugger();
}

static void first_run_prompt_post_show(struct modal_config *modal)
{
    t_config *cfg = config_get_global_cfg();

    cfg->general.first_use = 0;
    config_save_general(config_get_global_table(), KCFG_GENERAL_FST_USE);

    if (modal->result == 1)
        guitop_show_setup_menu();
}

static void gui_queue_first_run_prompt(int error)
{
    if (error)
        return;

    if (config_get_global_cfg()->general.first_use)
        gui_first_run_prompt_pending = 1;
}

/*
 * Replace whatever modal is up with a "couldn't seed the profiles" error so a
 * failed first-run copy is reported instead of silently proceeding.
 */
static void gui_show_seed_error(void)
{
    struct modal_config *modal = guitop_get_modal_cfg();

    memset(modal, 0, sizeof(struct modal_config));
    modal->modal_type = MODAL_MESSAGE;
    modal->modal_active = 1;
    modal->msg_type = KAT_MSG_TYPE_ERROR;
    strcpy(modal->title, "Setup Error");
    strcpy(modal->cancel_btn_label, "OK");
    strcpy(modal->msg,
           "Some default profiles could not be set up.\n"
           "\n"
           "Profiles may be missing or incomplete; see the log for details.");
    message_init(modal);
}

/*
 * First-run profile seeding splash.
 */
static void gui_maybe_run_seed_splash(void)
{
    static int seed_state = 0;   /* 0 idle, 1 copying (splash up), 2 done */
    struct modal_config *modal;

    if (seed_state == 2) {
        /* A later re-arm (e.g. factory reset) needs the splash to run again. */
        if (util_first_use_seed_pending())
            seed_state = 0;
        else
            return;
    }

    if (seed_state == 0) {
        if (!util_first_use_seed_pending())
            return;                          /* nothing deferred */

        modal = guitop_get_modal_cfg();
        if (modal->modal_active)
            return;                          /* let any active modal clear first */

        if (pc_start_thread(util_first_use_seed_worker, "FirstUseSeed", NULL) == NULL) {
            /* Thread creation failed: run the copy synchronously so seeding
             * still happens and the splash logic does not hang forever
             * waiting on a worker that will never set _done.  This blocks the
             * frame, but only on the rare thread-spawn failure path. */
            util_first_use_seed_worker(NULL);
            if (util_first_use_seed_error())
                gui_show_seed_error();
            else if (util_factory_reset_done())
                guitop_show_toast_msg("Configuration reset. Restart to apply.");
            seed_state = 2;
            return;
        }

        memset(modal, 0, sizeof(struct modal_config));
        modal->modal_type = MODAL_WAIT;
        modal->modal_active = 1;
        modal->msg_type = KAT_MSG_TYPE_NONE;
        strcpy(modal->title, "Please Wait");
        strcpy(modal->msg,
               "Setting up default profiles.\n"
               "\n"
               "Please wait...");
        message_init(modal);
        seed_state = 1;
        return;
    }

    /* seed_state == 1: copy running — dismiss the splash once it completes. */
    if (util_first_use_seed_done()) {
        modal = guitop_get_modal_cfg();
        if (modal->modal_type == MODAL_WAIT)
            modal->modal_active = 0;
        if (util_first_use_seed_error())
            gui_show_seed_error();
        else if (util_factory_reset_done())
            guitop_show_toast_msg("Configuration reset. Restart to apply.");
        seed_state = 2;
    }
}

static void gui_maybe_show_first_run_prompt(void)
{
    struct modal_config *modal;

    if (!gui_first_run_prompt_pending)
        return;

    modal = guitop_get_modal_cfg();
    if (modal->modal_active)
        return;

    gui_first_run_prompt_pending = 0;

    memset(modal, 0, sizeof(struct modal_config));
    modal->modal_type = MODAL_MESSAGE_CHOOSE;
    modal->modal_active = 1;
    modal->msg_type = KAT_MSG_TYPE_NONE;
    strcpy(modal->title, "First Run");
    strcpy(modal->accept_btn_label, "Ok");
    strcpy(modal->cancel_btn_label, "Cancel");
    strcpy(modal->msg,
           "Run through first setup? (recommended): \n"
           "      \xE2\x80\xA2 Set ROM directories \n"
           "      \xE2\x80\xA2 Download a ROM set \n"
           "      \xE2\x80\xA2 Setup mouse as trackball or paddles \n"
           "         for specific games \n"
           "      \xE2\x80\xA2 Setup two stick games \n"
           "Any and all optional       ");
    modal->post_show = first_run_prompt_post_show;

    message_init(modal);
}

static int nk_sdl_fail(void)
{
    SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Error: %s", SDL_GetError());
    return 1;
}

/* Restore the renderer's full-output coordinate system.  The game renderer
 * shares this SDL_Renderer with the GUI, so no GUI viewport may leak out of a
 * frame (or into the next safe-area query). */
static void gui_reset_render_coordinates(void)
{
    if (!renderer)
        return;

    SDL_SetRenderScale(renderer, 1.0f, 1.0f);
    SDL_SetRenderViewport(renderer, NULL);
}

static void gui_update_logical_canvas(void)
{
    int output_w = 0;
    int output_h = 0;
    int logical_w;
    int logical_h;
    SDL_Rect safe_area;

    if (g_ui_scale <= 0.0f)
        g_ui_scale = 1.0f;

    /* Start from the full output, then apply the GUI scale before asking SDL
     * for the safe area.  Viewport coordinates participate in render scaling,
     * so both the queried rectangle and the viewport we later set must use the
     * same logical coordinate system. */
    gui_reset_render_coordinates();

    if (!renderer ||
        !SDL_GetCurrentRenderOutputSize(renderer, &output_w, &output_h) ||
        output_w <= 0 || output_h <= 0) {
        output_w = KAT_WINDOW_MIN_WIDTH;
        output_h = KAT_WINDOW_MIN_HEIGHT;
        if (win)
            SDL_GetWindowSize(win, &output_w, &output_h);
    }

    render_output_w = output_w > 0 ? output_w : 1;
    render_output_h = output_h > 0 ? output_h : 1;
    logical_w = (int)(render_output_w / g_ui_scale);
    logical_h = (int)(render_output_h / g_ui_scale);
    if (logical_w < 1)
        logical_w = 1;
    if (logical_h < 1)
        logical_h = 1;

    if (renderer)
        SDL_SetRenderScale(renderer, g_ui_scale, g_ui_scale);

    safe_area.x = 0;
    safe_area.y = 0;
    safe_area.w = logical_w;
    safe_area.h = logical_h;

    if (renderer) {
        SDL_Rect reported;
        if (SDL_GetRenderSafeArea(renderer, &reported) &&
            reported.w > 0 && reported.h > 0) {
            safe_area = reported;
        }
    }

#ifdef __ANDROID__
    /* On gesture-navigation phones SDL's safe area includes the left/right
     * back-swipe zones (78 px per side on the Pixel 8).  Shrinking the entire
     * GUI for those zones leaves conspicuous gutters and shortens full-width
     * visuals such as the top menu and color band.  Keep the vertical insets
     * that protect against status/cutout/navigation bars, but let the phone UI
     * use the full horizontal canvas.  Android TV keeps SDL's horizontal safe
     * area because it represents real overscan rather than swipe gestures. */
    if (!pc_is_android_tv()) {
        int has_top_inset = safe_area.y > 0;
        int has_bottom_inset = safe_area.y + safe_area.h < logical_h;
        int inset_left;
        int inset_top;
        int inset_right;
        int inset_bottom;

        /* Android 15+ edge-to-edge windows expose both the visible system bars
         * and larger gesture-only regions to SDL.  Use Android's visible bar /
         * cutout values for vertical layout.  Only do this when SDL reported a
         * corresponding inset: older Android versions commonly give SDL an
         * already-inset app surface, and adding WindowInsets there would apply
         * the status/navigation bar spacing twice. */
        if ((has_top_inset || has_bottom_inset) &&
            pc_get_android_content_insets(&inset_left, &inset_top,
                                          &inset_right, &inset_bottom)) {
            int corrected_top = has_top_inset
                    ? (int)ceilf(inset_top / g_ui_scale) : 0;
            int corrected_bottom = has_bottom_inset
                    ? (int)ceilf(inset_bottom / g_ui_scale) : 0;

            safe_area.y = corrected_top;
            safe_area.h = logical_h - corrected_top - corrected_bottom;
        }

        safe_area.x = 0;
        safe_area.w = logical_w;
    }
#endif

    /* Be defensive about platform backends returning an area just outside the
     * current output during a resize/orientation transition. */
    if (safe_area.x < 0) {
        safe_area.w += safe_area.x;
        safe_area.x = 0;
    }
    if (safe_area.y < 0) {
        safe_area.h += safe_area.y;
        safe_area.y = 0;
    }
    if (safe_area.x >= logical_w || safe_area.y >= logical_h ||
        safe_area.w <= 0 || safe_area.h <= 0) {
        safe_area.x = 0;
        safe_area.y = 0;
        safe_area.w = logical_w;
        safe_area.h = logical_h;
    } else {
        if (safe_area.w > logical_w - safe_area.x)
            safe_area.w = logical_w - safe_area.x;
        if (safe_area.h > logical_h - safe_area.y)
            safe_area.h = logical_h - safe_area.y;
    }

    gui_render_area = safe_area;
    window_w = gui_render_area.w;
    window_h = gui_render_area.h;
    if (window_w < 1)
        window_w = 1;
    if (window_h < 1)
        window_h = 1;

    /* This function updates cached geometry; it does not own a rendering
     * scope.  Leave the shared renderer in its full-output coordinate system
     * for setup/rebuild callers as well as for the end-of-frame contract. */
    gui_reset_render_coordinates();
}

/* Make Nuklear's (0,0) the safe area's upper-left corner.  All existing menu
 * and modal code can continue using its local logical window_w/window_h while
 * SDL offsets the actual drawing and converted pointer input past system bars,
 * display cutouts, rounded corners, gesture areas, and TV overscan. */
static int gui_apply_render_coordinates(void)
{
    if (!renderer)
        return 0;

    if (!SDL_SetRenderScale(renderer, g_ui_scale, g_ui_scale)) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                    "GUI scale: SDL_SetRenderScale failed: %s",
                    SDL_GetError());
        return 0;
    }
    if (!SDL_SetRenderViewport(renderer, &gui_render_area)) {
        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                    "GUI safe area: SDL_SetRenderViewport failed: %s",
                    SDL_GetError());
        return 0;
    }
    return 1;
}

static float gui_get_auto_scale(void)
{
    float scale = 1.0f;

    if (win)
        scale = SDL_GetWindowDisplayScale(win);
    if (scale < 1.05f)
        scale = 1.0f;

    /* High-res desktops (e.g. 4K at OS 100%) report displayScale ~1.0, leaving
     * the GUI tiny.  When the OS asks for no DPI scaling, derive a floor from
     * the actual pixel height against a 1440p baseline (where the layout is
     * tuned): 1080p -> 1.0, 1440p -> 1.0, 4K -> 1.5, 5K -> 2.0.  Each keeps a
     * 2560-wide logical canvas at/above 1440p, matching native 1440p. */
    if (scale <= 1.0f && renderer) {
        int output_w = 0, output_h = 0;
        if (SDL_GetCurrentRenderOutputSize(renderer, &output_w, &output_h) &&
            output_h > 0) {
            float res_scale = (float)output_h / 1440.0f;
            if (res_scale > scale)
                scale = res_scale;
        }
    }

    /* Android TV reports ~1.0 displayScale, which leaves the GUI far too small
     * for a 10-foot living-room view.  Raise it to a floor.  TV is always
     * landscape (no narrow-portrait clipping), so the floor sits below the cap
     * below and only enlarges text. */
    if (pc_is_android_tv() && scale < GUI_TV_MIN_SCALE)
        scale = GUI_TV_MIN_SCALE;

    /* Cap auto magnification: at the device's full content scale (e.g. 2.5x on
     * a ~400 DPI phone) wide widget rows overflow the narrow portrait logical
     * canvas (pixels/scale).  Capping trades a little text size for more layout
     * width so portrait content isn't clipped. */
    if (scale > GUI_MAX_SCALE)
        scale = GUI_MAX_SCALE;

    return scale;
}

static float gui_get_configured_scale(void)
{
    t_config *cfg = config_get_global_cfg();
    int scale = cfg->video.gui_scale;

    if (scale <= 0)
        return gui_get_auto_scale();
    if (scale < 25)
        scale = 25;
    if (scale > 300)
        scale = 300;

    return (float)scale / 100.0f;
}

static void gui_apply_configured_scale(void)
{
    g_ui_scale = gui_get_configured_scale();
    gui_update_logical_canvas();
    theme_update_window_columns(window_w);
}

static void gui_build_fonts_and_theme(void)
{
    struct nk_font_atlas *atlas;
    struct nk_font_config config = nk_font_config(0);
    struct nk_font_config icon_config = nk_font_config(18);
    struct nk_font_config large_config;
    static const nk_rune icon_range[] = {
        0xF013, 0xF013, /* gear */
        0xF028, 0xF028, /* volume-high */
        0xF03A, 0xF03A, /* list */
        0xF04D, 0xF04D, /* stop */
        0xF053, 0xF053, /* chevron-left */
        0xF06E, 0xF06E, /* eye */
        0xF071, 0xF071, /* warning */
        0xF07B, 0xF07B, /* folder */
        0xF084, 0xF084, /* key */
        0xF0C7, 0xF0C9, /* save, bars */
        0xF0DA, 0xF0DA, /* caret-right */
        0xF0E7, 0xF0E7, /* bolt */
        0xF111, 0xF111, /* circle */
        0xF11B, 0xF11B, /* gamepad */
        0xF15B, 0xF15B, /* file */
        0xF171, 0xF171, /* error */
        0xF1DE, 0xF1DE, /* sliders */
        0xF1F8, 0xF1F8, /* trash */
        0xF26C, 0xF26C, /* tv */
        0xF2DB, 0xF2DB, /* microchip */
        0xF714, 0xF714, /* crash */
        0
    };
    /* Default Latin range plus U+2022 BULLET for list markers. */
    static const nk_rune text_range[] = {0x0020, 0x00FF, 0x2022, 0x2022, 0};
    float font_scale = g_ui_scale;

    config.range = text_range;
    icon_config.range = icon_range;
    icon_config.merge_mode = nk_true;

    /* Horizontal oversampling (default 3x) sharpens small text but is wasted on
     * large/solid glyphs and triples their atlas footprint.  The FA icons and
     * the big/Atarian title fonts don't perceptibly benefit, so bake them at 1x
     * to keep the atlas within GL_MAX_TEXTURE_SIZE on GLES2 GPUs (Fire TV =
     * 4096).  font_normal/font_small keep 3x for crisp body text. */
    icon_config.oversample_h = 1;
    large_config = config;
    large_config.oversample_h = 1;

    atlas = nk_sdl_font_stash_begin(ctx);
    g_theme.font_normal = nk_font_atlas_add_from_memory(atlas, Inter_SemiBold_ttf, Inter_SemiBold_ttf_len, 20 * font_scale, &config);
    g_theme.font_icons = nk_font_atlas_add_from_memory(atlas, fa_solid_900_ttf, fa_solid_900_ttf_len, 20 * font_scale, &icon_config);
    g_theme.font_small = nk_font_atlas_add_from_memory(atlas, Inter_SemiBold_ttf, Inter_SemiBold_ttf_len, 12 * font_scale, &config);
    g_theme.font_big = nk_font_atlas_add_from_memory(atlas, Inter_SemiBold_ttf, Inter_SemiBold_ttf_len, 26 * font_scale, &large_config);
    //g_theme.font_atari = nk_font_atlas_add_from_memory(atlas, Erasdemi_ttf, Erasdemi_ttf_len, 52 * font_scale, &config);
    g_theme.font_atari = nk_font_atlas_add_from_memory(atlas, SfAtarianSystem_ttf, SfAtarianSystem_ttf_len, 52 * font_scale, &large_config);
    nk_sdl_font_stash_end(ctx);

    if (font_scale != 1.0f) {
        g_theme.font_small->handle.height /= font_scale;
        g_theme.font_big->handle.height /= font_scale;
        g_theme.font_atari->handle.height /= font_scale;
        g_theme.font_normal->handle.height /= font_scale;
    }

    nk_style_set_font(ctx, &g_theme.font_normal->handle);
    theme_init(ctx);
}

static int gui_setup_window(void)
{
    t_config *cfg = config_get_global_cfg();
    int init_w = cfg->video.width  >= KAT_WINDOW_MIN_WIDTH  ? cfg->video.width  : KAT_WINDOW_MIN_WIDTH;
    int init_h = cfg->video.height >= KAT_WINDOW_MIN_HEIGHT ? cfg->video.height : KAT_WINDOW_MIN_HEIGHT;

    win = pc_set_window(&cfg->video, init_w, init_h);
    if (!win) return nk_sdl_fail();

    renderer = pc_get_renderer(1);
    if (!renderer) return nk_sdl_fail();

    SDL_SetRenderLogicalPresentation(renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
    SDL_ShowCursor();
    SDL_SetWindowMouseGrab(win, false);
    SDL_SetWindowRelativeMouseMode(win, false);

    /* Scale the whole GUI by auto display scale or by the user's manual
     * percentage. SDL_SetRenderScale magnifies every draw call, so Nuklear keeps
     * laying out in a logical canvas of pixels/scale and menu code stays in
     * logical coordinates. */
    gui_apply_configured_scale();

    return 0;
}

static int gui_init(int debug_flag)
{
    gui_debug_mode = debug_flag;
    gui_exit_requested = 0;
    gui_launch_requested = 0;
    gui_back_requested = 0;
    gui_no_reset_requested = 0;
    gui_run_current = 0;

    if (gui_setup_window()) return nk_sdl_fail();

    /* Text input is managed per frame in the render loop: on Android it follows
     * Nuklear edit-field focus (so the on-screen keyboard only appears in a text
     * field); on desktop it is kept always-on so launcher / file-browser list
     * type-ahead works.  See the __ANDROID__ split near nk_sdl_update_TextInput. */

    /* Initialize Nuklear */
    ctx = nk_sdl_init(win, renderer, nk_sdl_allocator());
    if (!ctx) {
        return nk_sdl_fail();
    }

    gui_build_fonts_and_theme();

    gui_initialized = 1;
    gui_running = 1;

    return 0;
}

static int gui_rebuild_context(void)
{
    if (!gui_initialized || !ctx)
        return 0;

    nk_sdl_shutdown(ctx);
    ctx = NULL;

    gui_apply_configured_scale();

    ctx = nk_sdl_init(win, renderer, nk_sdl_allocator());
    if (!ctx) {
        gui_initialized = 0;
        return nk_sdl_fail();
    }

    gui_build_fonts_and_theme();
    guitop_invalidate_layout();
    return 0;
}

void gui_request_rebuild(void)
{
    gui_rebuild_requested = 1;
}

int gui_get_effective_scale_x100(void)
{
    int scale = (int)(g_ui_scale * 100.0f + 0.5f);

    if (scale < 25)
        scale = 25;
    if (scale > 300)
        scale = 300;
    return scale;
}

unsigned int gui_show_main(int error, int debug_flag)
{
    t_config *cfg = config_get_global_cfg();
    int effective_error = (error == ERR_ROM_LOAD && cfg->general.first_use) ? 0 : error;

    if (effective_error) {
        struct modal_config *modal = guitop_get_modal_cfg();

        memset(modal, 0, sizeof(struct modal_config));
        modal->modal_type = MODAL_MESSAGE;
        modal->modal_active = 1;
        modal->msg_type = KAT_MSG_TYPE_ERROR;
        strcpy(modal->title, "Error");
        strcpy(modal->cancel_btn_label, "OK");

        switch (effective_error) {
            case ERR_ROM_LOAD:
                snprintf(modal->msg, sizeof(modal->msg), "Failed to load ROM: %s", cfg->general.rom);
                break;
            case ERR_BASIC_LOAD:
                snprintf(modal->msg, sizeof(modal->msg), "Failed to load BASIC: %s", cfg->general.basic);
                break;
            case ERR_BIOS_LOAD:
                snprintf(modal->msg, sizeof(modal->msg), "Failed to load BIOS: %s", cfg->machine.bios);
                break;
            case ERR_CPU_CRASH:
                snprintf(modal->msg, sizeof(modal->msg), "CPU crash detected");
                if (pc_debugger_available()) {
                    modal->modal_type = MODAL_MESSAGE_CHOOSE;
                    strcpy(modal->accept_btn_label, "Debug");
                    modal->post_show = cpu_crash_post_show;
                }
                break;
            default:
                snprintf(modal->msg, sizeof(modal->msg), "Unknown error");
                break;
        }

        message_init(modal);
    }

    gui_queue_first_run_prompt(effective_error);

    pc_check_for_and_open_joysticks();

    if (gui_initialized) {
        gui_running = 1;
        gui_exit_requested = 0;
        gui_launch_requested = 0;
        gui_back_requested = 0;
        gui_no_reset_requested = 0;
        gui_run_current = 0;
        gui_can_go_back_flag = 1;

        /* Restore window and renderer state from game mode */
        if (gui_setup_window()) return nk_sdl_fail();

        /* Force Nuklear window to match actual window size */
        nk_window_set_bounds(ctx, "katGUI", nk_rect(0, 0, window_w, window_h));

        /* Window may have been resized in game mode — re-measure main spacer */
        guitop_invalidate_layout();

        /* Returning from in-game: the input-down that launched the game was seen
           by Nuklear, but the GUI stopped pumping events before the matching
           release, so ctx->input can still believe the mouse button or Enter key
           is held.  That stale 'down' swallows the first activation on re-entry
           (no fresh press transition), forcing a second click/press.  Clear the
           persistent down state so the first real activation registers cleanly. */
        for (int b = 0; b < NK_BUTTON_MAX; ++b) {
            ctx->input.mouse.buttons[b].down = nk_false;
            ctx->input.mouse.buttons[b].clicked = 0;
        }
        for (int k = 0; k < NK_KEY_MAX; ++k)
            ctx->input.keyboard.keys[k].down = nk_false;

        return 0;
    }

    int result = gui_init(debug_flag);
    if (result == 0 && error == 0)
        gui_can_go_back_flag = 1;
    return result;
}

#ifdef __ANDROID__
/* Drive Nuklear from a single finger: drag -> scroll, tap -> click.  Finger
 * coords are normalized across the full output; translate them into the GUI's
 * safe-area viewport and logical render scale. */
static void gui_handle_finger_event(struct nk_context *ctx, SDL_Event *evt)
{
    int input_w = render_output_w;
    int input_h = render_output_h;
    float fx;
    float fy;

    /* Finger coordinates are normalized window coordinates, not renderer
     * coordinates.  Ask SDL to apply the window, scale, and safe viewport
     * transforms so touch and drawing always share the exact same mapping. */
    if (win)
        SDL_GetWindowSize(win, &input_w, &input_h);
    fx = evt->tfinger.x * (float)input_w;
    fy = evt->tfinger.y * (float)input_h;
    if (!renderer ||
        !SDL_RenderCoordinatesFromWindow(renderer, fx, fy, &fx, &fy)) {
        float scale = g_ui_scale > 0.0f ? g_ui_scale : 1.0f;
        float logical_output_w = (float)render_output_w / scale;
        float logical_output_h = (float)render_output_h / scale;

        /* Preserve the same full-window -> safe-viewport transform when SDL's
         * coordinate helper is unavailable.  Coordinates outside the safe
         * area intentionally remain negative or beyond window_w/window_h. */
        fx = evt->tfinger.x * logical_output_w - (float)gui_render_area.x;
        fy = evt->tfinger.y * logical_output_h - (float)gui_render_area.y;
    }

    switch (evt->type) {
        case SDL_EVENT_FINGER_DOWN:
            if (g_touch_active)              /* ignore additional fingers */
                break;
            g_touch_active = 1;
            g_touch_finger = evt->tfinger.fingerID;
            g_touch_down_x = g_touch_last_x = fx;
            g_touch_down_y = g_touch_last_y = fy;
            g_touch_moved  = 0;
            g_touch_axis   = 0;
            nk_input_motion(ctx, (int)fx, (int)fy);   /* set hover, no press */
            break;

        case SDL_EVENT_FINGER_MOTION: {
            float dx, dy, tot_x, tot_y;
            if (!g_touch_active || evt->tfinger.fingerID != g_touch_finger)
                break;
            dx = fx - g_touch_last_x;
            dy = fy - g_touch_last_y;
            g_touch_last_x = fx;
            g_touch_last_y = fy;
            tot_x = fx - g_touch_down_x;
            tot_y = fy - g_touch_down_y;
            nk_input_motion(ctx, (int)fx, (int)fy);   /* keep hover under finger */

            if (!g_touch_moved &&
                (SDL_fabsf(tot_x) > GUI_TAP_SLOP || SDL_fabsf(tot_y) > GUI_TAP_SLOP)) {
                g_touch_moved = 1;
                /* Lock the axis, biased toward vertical (list scrolling is the
                 * common case and a vertical swipe often drifts sideways). */
                g_touch_axis = (SDL_fabsf(tot_x) > SDL_fabsf(tot_y) * 1.25f)
                                   ? GUI_AXIS_H : GUI_AXIS_V;
            }

            if (g_touch_moved) {
                /* Nuklear scales scroll by ~10% of the scrollbar extent, so feed
                 * ticks (~12/window per logical px) for a slightly-faster-than
                 * 1:1 finger-to-content feel.  Positive scroll_delta scrolls
                 * toward the top/left, the natural direction (content follows the
                 * finger).  Only the locked axis scrolls. */
                if (g_touch_axis == GUI_AXIS_H)
                    nk_input_scroll(ctx, nk_vec2(dx * (12.0f / (float)window_w), 0.0f));
                else
                    nk_input_scroll(ctx, nk_vec2(0.0f, dy * (12.0f / (float)window_h)));
            }
            break;
        }

        case SDL_EVENT_FINGER_UP:
            if (!g_touch_active || evt->tfinger.fingerID != g_touch_finger)
                break;
            if (!g_touch_moved) {
                /* Tap: press now, release next frame (a same-frame down+up reads
                 * as "not pressed" at nk_input_end). */
                nk_input_motion(ctx, (int)fx, (int)fy);
                nk_input_button(ctx, NK_BUTTON_LEFT, (int)fx, (int)fy, 1);
                g_tap_release_x = fx;
                g_tap_release_y = fy;
                g_tap_release_pending = 1;
            }
            g_touch_active = 0;
            break;

        default:
            break;
    }
}
#endif /* __ANDROID__ */

/* True while a finger swipe is actively scrolling.  List widgets that run their
 * own scroll smoothing/auto-scroll (e.g. the settings menu_list) check this and
 * let Nuklear's native scroll stand during a swipe, otherwise their smoothing
 * compounds with the injected nk_input_scroll and the list flies. */
int gui_touch_scroll_active(void)
{
#ifdef __ANDROID__
    return g_touch_active && g_touch_moved;
#else
    return 0;
#endif
}

unsigned int gui_run_frame(void)
{
    unsigned int status = 0;

    if (!gui_initialized) {
        if (gui_init(0) != 0) {
            return KSTATUS_EXIT;
        }
    }

    gui_maybe_run_seed_splash();
    gui_maybe_show_first_run_prompt();

    /* Safe-area insets can change without a window resize (system-bar
     * visibility, cutout emulation, rotation), so refresh them every frame. */
    gui_update_logical_canvas();
    if (!gui_apply_render_coordinates()) {
        gui_reset_render_coordinates();
        g_ui_scale = 1.0f;
        gui_update_logical_canvas();
        gui_apply_render_coordinates();
    }

    /* Process queued events */
    nk_input_begin(ctx);
#ifdef __ANDROID__
    /* Complete a tap click deferred from the previous frame (see FINGER_UP). */
    if (g_tap_release_pending) {
        nk_input_button(ctx, NK_BUTTON_LEFT,
                        (int)g_tap_release_x, (int)g_tap_release_y, 0);
        g_tap_release_pending = 0;
    }
#endif
    for (int i = 0; i < event_count; i++) {
#ifdef __ANDROID__
        /* Touch is handled directly as finger events (taps + scroll); never via
         * the synthetic mouse (disabled in pc_init, but guard regardless). */
        if (event_queue[i].type == SDL_EVENT_FINGER_DOWN ||
            event_queue[i].type == SDL_EVENT_FINGER_MOTION ||
            event_queue[i].type == SDL_EVENT_FINGER_UP) {
            gui_handle_finger_event(ctx, &event_queue[i]);
            continue;
        }
        if ((event_queue[i].type == SDL_EVENT_MOUSE_MOTION &&
             event_queue[i].motion.which == SDL_TOUCH_MOUSEID) ||
            ((event_queue[i].type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
              event_queue[i].type == SDL_EVENT_MOUSE_BUTTON_UP) &&
             event_queue[i].button.which == SDL_TOUCH_MOUSEID))
            continue;
#endif
        SDL_ConvertEventToRenderCoordinates(renderer, &event_queue[i]);

        if (event_queue[i].type == SDL_EVENT_QUIT) {
            gui_exit_requested = 1;
        }

        /* Check if input capture modal is active and wants to handle this event */
        if (menu_input_capture_is_active()) {
            if (menu_input_capture_process_event(&event_queue[i])) {
                /* Event was consumed by input capture, skip normal processing */
                continue;
            }
        }

        if (event_queue[i].type == SDL_EVENT_KEY_DOWN) {
            /* AC_BACK is the TV remote / Android navigation Back button (trapped
             * as a key via SDL_HINT_ANDROID_TRAP_BACK_BUTTON in pc_init).*/
            if (event_queue[i].key.key == SDLK_ESCAPE ||
                event_queue[i].key.scancode == SDL_SCANCODE_AC_BACK) {
                guitop_esc();
            }
        }
        gui_handle_joystick_nav(ctx, &event_queue[i]);
        nk_sdl_handle_event(ctx, &event_queue[i]);
    }
    event_count = 0;
    nk_input_end(ctx);

    /* Render GUI */
    guitop(ctx, window_w, window_h);

    if (!guitop_is_measuring()) {
#ifdef __ANDROID__
        /* Android: raise/hide the on-screen keyboard based on whether a Nuklear
         * edit field is focused this frame, so the soft keyboard only appears
         * when the user is actually in a text field. */
        nk_sdl_update_TextInput(ctx);
#else
        /* Desktop: keep text input on the whole time the GUI is up (there is no
         * on-screen keyboard to worry about) so list type-ahead works in the
         * launcher and file browser, which read ctx->input.keyboard.text but
         * are not Nuklear edit widgets.  Re-arm if a window recreate cleared it. */
        if (!SDL_TextInputActive(win))
            SDL_StartTextInput(win);
#endif

        /* Clear the entire edge-to-edge output, then restore the safe viewport
         * for Nuklear's important and interactive content. */
        gui_reset_render_coordinates();
        SDL_SetRenderDrawColor(renderer, g_theme.background.r, g_theme.background.g, g_theme.background.b, 255);
        SDL_RenderClear(renderer);
        gui_apply_render_coordinates();
        nk_sdl_render(ctx, NK_ANTI_ALIASING_ON);
        SDL_RenderPresent(renderer);
    } else {
        nk_clear(ctx);
    }

    gui_reset_render_coordinates();

    if (gui_rebuild_requested) {
        gui_rebuild_requested = 0;
        if (gui_rebuild_context() != 0)
            return KSTATUS_EXIT;
    }

    /* Build status flags */
    if (gui_exit_requested) {
        status |= KSTATUS_EXIT;
    }
    if (gui_launch_requested) {
        status |= KSTATUS_LAUNCH;
        if (gui_back_requested)
            status |= KSTATUS_BACK;
        else if (gui_no_reset_requested)
            status |= KSTATUS_LAUNCH_NO_RST;
        else
            gui_can_go_back_flag = 0;
        gui_running = 0;
    }
    if (gui_debug_mode || debugger_is_active()) {
        status |= KSTATUS_DEBUG;
    }
    if (debugger_wants_input()) {
        status |= KSTATUS_INPUT;
    }
    if (gui_running) {
        status |= KSTATUS_GUI;
    }
    if (gui_run_current) {
        status |= KSTATUS_RUN;
    }

    return status;
}

void gui_launch (int launch)
{
    if (launch == 2) {
        gui_launch_requested = 1;
        gui_back_requested = 1;
    }
    else if (launch == 3) {
        gui_launch_requested = 1;
        gui_no_reset_requested = 1;
    }
    else if (launch) {
        gui_launch_requested = 1;
    }
    else {
        gui_run_current = 1;
    }
}

int gui_can_go_back(void)
{
    return gui_can_go_back_flag;
}

void gui_set_can_go_back(int val)
{
    gui_can_go_back_flag = val;
}

void gui_push_event(SDL_Event *event)
{
    if (event_count < MAX_EVENTS) {
        event_queue[event_count++] = *event;
    }
}

void gui_exit(void)
{
    if (gui_initialized) {
        nk_sdl_shutdown(ctx);
        ctx = NULL;
        gui_initialized = 0;
        gui_running = 0;
    }
    menu_options_setup_exit();
}

void gui_set_run_current(int val)
{
    gui_run_current = val;
}

const char *gui_get_version(void)
{
    return "Nuklear 1.40.8";
}

/* Wrapper for menu callback - sets exit flag.  Signature must match the
   run_task function-pointer type; calling through a mismatched pointer
   type is undefined behavior. */
struct option_widget;
void gui_request_exit(struct option_widget *wgt, void *data)
{
    (void)wgt;
    (void)data;
    gui_exit_requested = 1;
}
