/******************************************************************************
*
* Copyright (C) 2026  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: menu_shader.c
*
* DESCRIPTION:  Video -> Shader submenu.  Treats bundle.toml as immutable
*               bundle metadata/defaults; kat5200.conf and the ROM profile are
*               the truth for the selected shader and any edited parameter
*               overrides.  All editing happens against a menu-local working
*               copy and is preview-only until the user saves.
*
******************************************************************************/
#include <math.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>

#include "menu.h"
#include "widget.h"
#include "modal.h"
#include "message.h"
#include "filebrowser.h"
#include "guitop.h"
#include "theme.h"
#include "nuklear.h"
#include "../interface/katconfig.h"
#include "../interface/sdl_if.h"
#include "../interface/shader_bundle.h"
#include "../interface/util.h"

extern struct theme g_theme;
extern t_sdl_if g_pc_if;

#define SHADER_MAX_BUNDLES 64
#define SHADER_MAX_PARAMS  256
#define SHADER_MAX_GROUPS  48
#define PREVIEW_RIGHT_INSET 30

/* value must be the first member: the widget reads/writes *(int*)wgt->data,
   and our run_task/get_string recover the owning stage by casting that same
   pointer back (offset 0). */
struct shader_param_stage {
    int value;
    t_shader_bundle_param *param;
};

struct shader_group_state {
    char name[48];
    int collapsed;
};

/* ---- menu-local working state (preview-only until save) ---- */
static t_shader_bundle s_bundle;
static int  s_bundle_loaded = 0;

static char *s_bundle_ids[SHADER_MAX_BUNDLES];
static int   s_bundle_count = 0;

static int   s_has_external = 0;
static char  s_external_dir[KAT_PATH_MAX];

static int   s_sel_index = 0;

static struct shader_param_stage s_param_stage[SHADER_MAX_PARAMS];
static unsigned long long s_param_rom[SHADER_MAX_PARAMS];
static int   s_param_count = 0;
static struct shader_group_state s_groups[SHADER_MAX_GROUPS];
static int   s_group_count = 0;

static unsigned long long s_shader_video_mask = 0;
static int   s_dirty = 0;
static int   s_initialized = 0;
static int   s_restart_warning_shown = 0;
static char  s_status[256];

/* CPU fallback preview texture (unshaded current frame) */
static SDL_Texture *s_cpu_tex = NULL;
static int          s_cpu_w   = 0;
static int          s_cpu_h   = 0;

static char * shader_param_string ( struct option_widget *wgt, char *label );
static void shader_param_changed ( struct option_widget *wgt, void *data );
static int shader_group_index ( const char *name );

/* ---- param <-> integer staging helpers ---- */

static int param_is_bool ( const t_shader_bundle_param *p )
{
    if ( p->type && ( !strcmp(p->type, "bool") || !strcmp(p->type, "boolean") ) )
        return 1;
    return ( p->min == 0.0f && p->max == 1.0f && p->step == 1.0f );
}

static int param_to_int ( const t_shader_bundle_param *p )
{
    if ( p->step <= 0.0f )
        return 0;
    return (int) lroundf ( (p->val - p->min) / p->step );
}

static float param_from_int ( const t_shader_bundle_param *p, int v )
{
    return p->min + (float) v * p->step;
}

static int param_int_max ( const t_shader_bundle_param *p )
{
    if ( p->step <= 0.0f )
        return 0;
    return (int) lroundf ( (p->max - p->min) / p->step );
}

static const char * shader_param_label ( const t_shader_bundle_param *p )
{
    const char *label;

    if ( !p )
        return "param";

    label = p->label ? p->label : p->name;
    while ( label && isspace((unsigned char) *label) )
        label++;

    if ( label && *label )
        return label;
    return p->name ? p->name : "param";
}

static int shader_param_has_group ( const t_shader_bundle_param *p )
{
    return p && p->group && p->group[0];
}

static int shader_group_name_matches ( const t_shader_bundle_param *p,
        const char *name )
{
    return shader_param_has_group(p) && name && !strcmp ( p->group, name );
}

static int shader_param_group_bucketed ( const t_shader_bundle_param *p )
{
    return shader_param_has_group(p) && shader_group_index ( p->group ) >= 0;
}

static void shader_reset_groups ( void )
{
    memset ( s_groups, 0, sizeof(s_groups) );
    s_group_count = 0;
}

static int shader_group_index ( const char *name )
{
    if ( !name || !name[0] )
        return -1;

    for ( int i = 0; i < s_group_count; ++i ) {
        if ( !strcmp ( s_groups[i].name, name ) )
            return i;
    }

    return -1;
}

static void shader_build_groups ( void )
{
    shader_reset_groups ();

    for ( int i = 0; i < s_param_count && s_group_count < SHADER_MAX_GROUPS; ++i ) {
        t_shader_bundle_param *param = s_param_stage[i].param;
        struct shader_group_state *group;

        if ( !shader_param_has_group(param) )
            continue;
        if ( shader_group_index(param->group) >= 0 )
            continue;

        group = &s_groups[s_group_count++];
        strncpy ( group->name, param->group, sizeof(group->name) - 1 );
        group->name[sizeof(group->name) - 1] = '\0';
        group->collapsed = 1;
    }
}

static void shader_toggle_group ( struct option_widget *wgt, void *data )
{
    int *collapsed = (int *) data;

    (void) wgt;

    if ( collapsed )
        *collapsed = !*collapsed;
}

static int shader_append_param_widget ( struct option_widget *wid_ary,
        int *item_index, int param_index )
{
    t_shader_bundle_param *param;
    const char *name;

    if ( !item_index || *item_index >= MAX_OPTIONS - 4 )
        return 0;
    if ( param_index < 0 || param_index >= s_param_count )
        return 0;

    param = s_param_stage[param_index].param;
    if ( !param )
        return 0;

    ++*item_index;
    name = shader_param_label ( param );

    strncpy ( wid_ary[*item_index].label, name,
            sizeof(wid_ary[*item_index].label) - 1 );
    wid_ary[*item_index].label[sizeof(wid_ary[*item_index].label) - 1] = '\0';
    wid_ary[*item_index].data = &s_param_stage[param_index].value;
    wid_ary[*item_index].get_string = shader_param_string;
    wid_ary[*item_index].run_task = shader_param_changed;
    wid_ary[*item_index].p_source_mask = &s_param_rom[param_index];
    wid_ary[*item_index].mask_value = 1;
    wid_ary[*item_index].divider = param->divider;

    if ( param_is_bool(param) ) {
        wid_ary[*item_index].widget_type = WIDGET_TYPE_TOGGLE;
        wid_ary[*item_index].min = 0;
        wid_ary[*item_index].max = 1;
    }
    else {
        wid_ary[*item_index].widget_type = WIDGET_TYPE_SLIDER;
        wid_ary[*item_index].min = 0;
        wid_ary[*item_index].max = param_int_max ( param );
    }

    return 1;
}

static int shader_append_group_header ( struct option_widget *wid_ary,
        int *item_index, int group_index )
{
    struct shader_group_state *group;

    if ( !item_index || *item_index >= MAX_OPTIONS - 4 )
        return 0;
    if ( group_index < 0 || group_index >= s_group_count )
        return 0;

    group = &s_groups[group_index];
    ++*item_index;

    wid_ary[*item_index].widget_type = WIDGET_TYPE_LABEL;
    snprintf ( wid_ary[*item_index].label, sizeof(wid_ary[*item_index].label),
            "%s %s", group->collapsed ? "[+]" : "[-]", group->name );
    wid_ary[*item_index].divider = 1;
    wid_ary[*item_index].data = &group->collapsed;
    wid_ary[*item_index].run_task = shader_toggle_group;

    return 1;
}

/* ---- working bundle load / param staging ---- */

/* RetroArch presets share a single uniform across passes by name, so the same
   "#pragma parameter" can appear in several passes.  kat5200 stores a separate
   copy per pass; these helpers collapse same-named params into one GUI control
   and keep every copy in sync. */
static int shader_name_listed ( const char *name, const char *const *names,
        int count )
{
    if ( !name )
        return 0;
    for ( int i = 0; i < count; ++i )
        if ( names[i] && !strcmp(names[i], name) )
            return 1;
    return 0;
}

static void shader_propagate_param_value ( const char *name, float val )
{
    if ( !name )
        return;
    for ( int i = 0; i < s_bundle.num_passes; ++i ) {
        t_shader_bundle_pass *pass = &s_bundle.passes[i];
        for ( int j = 0; j < pass->num_params; ++j ) {
            t_shader_bundle_param *p = &pass->params[j];
            if ( !p->name || strcmp(p->name, name) )
                continue;
            p->val = ( val < p->min ) ? p->min : ( val > p->max ) ? p->max : val;
        }
    }
}

static void shader_build_param_stage ( const float *snapshot, int snap_count,
        int have_rom )
{
    const char *seen[SHADER_MAX_PARAMS];
    int nseen = 0;
    int k = 0;

    s_param_count = 0;
    shader_reset_groups ();

    /* Stale param widgets from a previously selected bundle can still be
       iterated for the remainder of the current GUI frame (the widget array is
       only rebuilt next frame).  Their data points at s_param_stage[] entries,
       so clear every slot's param here: any leftover entry then trips the
       NULL guard in the string/run-task callbacks instead of dereferencing a
       param in the freed bundle. */
    for ( int i = 0; i < SHADER_MAX_PARAMS; ++i ) {
        s_param_stage[i].param = NULL;
        s_param_stage[i].value = 0;
    }

    if ( !s_bundle_loaded )
        return;

    for ( int i = 0; i < s_bundle.num_passes && k < SHADER_MAX_PARAMS; ++i ) {
        t_shader_bundle_pass *pass = &s_bundle.passes[i];

        for ( int j = 0; j < pass->num_params && k < SHADER_MAX_PARAMS; ++j ) {
            t_shader_bundle_param *param = &pass->params[j];

            if ( !param->name )
                continue;

            /* Collapse same-named params (shared uniform) into one control. */
            if ( shader_name_listed(param->name, seen, nseen) )
                continue;
            seen[nseen++] = param->name;

            s_param_stage[k].param = param;
            s_param_stage[k].value = param_to_int ( param );

            if ( have_rom && snapshot && k < snap_count )
                s_param_rom[k] = ( fabsf(param->val - snapshot[k]) > 0.000001f )
                                 ? 1ULL : 0ULL;
            else
                s_param_rom[k] = 0ULL;

            k++;
        }
    }

    s_param_count = k;
    shader_build_groups ();
}

static void shader_load_working ( const char *selector )
{
    char dir[KAT_PATH_MAX];
    float snapshot[SHADER_MAX_PARAMS];
    int snap_count = 0;
    t_toml_node *rom;

    /* The bundle is changing under the preview pipeline; drop its GPU state. */
    pc_shader_preview_free ();
    shader_bundle_free ( &s_bundle );
    s_bundle_loaded = 0;
    s_param_count = 0;
    shader_reset_groups ();
    s_status[0] = '\0';

    if ( !selector || !strlen(selector) || !strcmp(selector, SHADER_NONE) ) {
        snprintf ( s_status, sizeof(s_status), "Shader disabled." );
        return;
    }

    if ( pc_resolve_shader_bundle(selector, dir, sizeof(dir)) ) {
        snprintf ( s_status, sizeof(s_status), "Bundle not found: %s",
                selector );
        return;
    }

    if ( shader_bundle_load(dir, &s_bundle) ) {
        snprintf ( s_status, sizeof(s_status), "Failed to load bundle: %s",
                dir );
        return;
    }

    s_bundle_loaded = 1;

    /* Layer overrides: bundle defaults, then global config, then ROM profile. */
    config_load_shader_bundle_info ( config_get_global_table(), &s_bundle, 1 );

    const char *snap_seen[SHADER_MAX_PARAMS];
    int snap_nseen = 0;
    for ( int i = 0; i < s_bundle.num_passes &&
            snap_count < SHADER_MAX_PARAMS; ++i ) {
        t_shader_bundle_pass *pass = &s_bundle.passes[i];
        for ( int j = 0; j < pass->num_params &&
                snap_count < SHADER_MAX_PARAMS; ++j ) {
            if ( !pass->params[j].name )
                continue;
            /* Dedupe identically to shader_build_param_stage so snapshot[k]
               aligns with the collapsed param list (ROM-override markers). */
            if ( shader_name_listed(pass->params[j].name, snap_seen, snap_nseen) )
                continue;
            snap_seen[snap_nseen++] = pass->params[j].name;
            snapshot[snap_count++] = pass->params[j].val;
        }
    }

    rom = config_get_rom_table ();
    if ( rom )
        config_load_shader_bundle_info ( rom, &s_bundle, 0 );

    shader_build_param_stage ( snapshot, snap_count, rom != NULL );

    if ( s_bundle.num_passes != 1 )
        snprintf ( s_status, sizeof(s_status), "Loaded %s (%d passes).",
                s_bundle.id ? s_bundle.id : selector, s_bundle.num_passes );
    else
        snprintf ( s_status, sizeof(s_status), "Loaded %s.",
                s_bundle.id ? s_bundle.id : selector );
}

/* ---- selection list mapping ---- */

static int shader_external_index ( void )
{
    return s_has_external ? ( 1 + s_bundle_count ) : -1;
}

static int shader_option_count ( void )
{
    return 1 + s_bundle_count + ( s_has_external ? 1 : 0 );
}

static const char * shader_selector_for_index ( int idx )
{
    if ( idx <= 0 )
        return SHADER_NONE;
    if ( idx >= 1 && idx <= s_bundle_count )
        return s_bundle_ids[idx - 1];
    if ( s_has_external && idx == shader_external_index() )
        return s_external_dir;
    return SHADER_NONE;
}

static const char * shader_path_basename ( const char *path )
{
    const char *slash = path;
    const char *p;

    for ( p = path; *p; ++p ) {
        if ( *p == '/' || *p == '\\' )
            slash = p + 1;
    }
    return slash;
}

/* ---- widget string callbacks ---- */

/* Format the shader selector value for a given index into buf (size n).
   Shared by the widget value callback and the column-width measurement so the
   measured text matches exactly what gets rendered. */
static void shader_format_selector ( int idx, char *buf, size_t n )
{
    const char *sel = shader_selector_for_index ( idx );

    if ( idx <= 0 )
        snprintf ( buf, n, "None" );
    else if ( s_has_external && idx == shader_external_index() )
        snprintf ( buf, n, "External: %s", shader_path_basename(sel) );
    else
        snprintf ( buf, n, "%s", sel );
}

static char * shader_sel_string ( struct option_widget *wgt, char *label )
{
    shader_format_selector ( *(int *)(wgt->data), label, WIDGET_VALUE_STR_SIZE );
    return label;
}

/* Widest rendered shader-selector value across the full selectable range, in
   pixels at the current font. Used by guitop's autosize pre-pass to size the
   select value column so it does not jump as the user cycles selections. */
float menu_shader_select_value_width ( struct nk_context *ctx )
{
    char buf[WIDGET_VALUE_STR_SIZE];
    int count = shader_option_count();
    float max_w = 0.0f;

    if ( !ctx || !ctx->style.font )
        return 0.0f;

    for ( int idx = 0; idx < count; ++idx ) {
        float w;
        shader_format_selector ( idx, buf, sizeof(buf) );
        w = ctx->style.font->width ( ctx->style.font->userdata,
                                     ctx->style.font->height,
                                     buf, (int) strlen(buf) );
        if ( w > max_w )
            max_w = w;
    }

    return max_w;
}

static char * shader_param_string ( struct option_widget *wgt, char *label )
{
    struct shader_param_stage *s = (struct shader_param_stage *)(wgt->data);
    float v;

    if ( !s || !s->param ) {
        strcpy ( label, "" );
        return label;
    }

    v = param_from_int ( s->param, s->value );

    if ( param_is_bool(s->param) )
        strcpy ( label, v >= 0.5f ? "On" : "Off" );
    else
        sprintf ( label, shader_get_float_format(s->param->step), v );

    return label;
}

/* ---- widget run_task callbacks ---- */

static void shader_show_restart_warning_if_needed ( const char *sel )
{
    struct modal_config *modal;

    if ( s_restart_warning_shown )
        return;
    if ( !sel || !strlen(sel) || !strcmp(sel, SHADER_NONE) )
        return;
    if ( pc_get_gpu_device() )
        return;

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

    memset ( modal, 0, sizeof(struct modal_config) );
    modal->modal_type = MODAL_MESSAGE;
    modal->modal_active = 1;
    modal->msg_type = KAT_MSG_TYPE_WARN;
    strcpy ( modal->title, "Shader Restart Required" );
    strcpy ( modal->cancel_btn_label, "OK" );
    strcpy ( modal->msg,
            "Using shaders requires restart. Save shader and restart kat5200" );
    message_init ( modal );

    s_restart_warning_shown = 1;
}

static void shader_sel_changed ( struct option_widget *wgt, void *data )
{
    t_config *cfg = config_get_global_cfg ();
    int idx = *(int *)data;
    const char *sel = shader_selector_for_index ( idx );
    char dir[KAT_PATH_MAX];

    (void) wgt;

    /* If the chosen bundle no longer resolves (most often a saved external
       directory that has since moved or lives on another machine), don't keep
       a dead reference.  Tell the user, fall back to None, and blank the
       config value so the next save clears the stale 'shader' key from
       kat5200.conf (config_save_video deletes the key for an empty string). */
    if ( sel && strlen(sel) && strcmp(sel, SHADER_NONE) &&
            pc_resolve_shader_bundle ( sel, dir, sizeof(dir) ) ) {
        const char *name = ( s_has_external && idx == shader_external_index() )
                ? shader_path_basename ( sel ) : sel;
        guitop_show_toast_msg ( "Shader bundle missing — reset to None." );
        snprintf ( s_status, sizeof(s_status),
                "Bundle not found: %s — reset to None.", name );

        s_sel_index = 0;
        util_alloc_string ( &cfg->video.shader, "" );
        s_shader_video_mask |= KCFG_VIDEO_SHADER;
        s_dirty = 1;

        shader_load_working ( SHADER_NONE );
        return;
    }

    util_alloc_string ( &cfg->video.shader, sel );
    s_shader_video_mask |= KCFG_VIDEO_SHADER;
    s_dirty = 1;

    shader_show_restart_warning_if_needed ( sel );
    shader_load_working ( sel );
}

static void shader_reset_params ( struct option_widget *wgt, void *data )
{
    (void) wgt;
    (void) data;

    if ( !s_bundle_loaded )
        return;

    shader_bundle_reset_params ( &s_bundle );
    shader_build_param_stage ( NULL, 0, 0 );
    s_dirty = 1;
    snprintf ( s_status, sizeof(s_status), "Parameters reset to defaults." );
}

static void shader_param_changed ( struct option_widget *wgt, void *data )
{
    struct shader_param_stage *s = (struct shader_param_stage *)data;

    (void) wgt;

    if ( !s || !s->param )
        return;

    /* Write the value to every same-named copy across passes (shared uniform);
       this also updates s->param itself and clamps each to its own range. */
    shader_propagate_param_value ( s->param->name,
            param_from_int ( s->param, s->value ) );

    s_dirty = 1;
}

static void shader_dir_result ( struct modal_config *cfg )
{
    t_config *gcfg = config_get_global_cfg ();
    const char *path;
    t_shader_bundle tmp;

    if ( !cfg->result )
        return;

    path = filebrowser_result ();
    if ( !path || !strlen(path) )
        return;

    shader_bundle_init ( &tmp );
    if ( shader_bundle_load(path, &tmp) ) {
        shader_bundle_free ( &tmp );
        guitop_show_toast_msg ( "Not a valid shader bundle directory." );
        return;
    }
    shader_bundle_free ( &tmp );

    s_has_external = 1;
    snprintf ( s_external_dir, sizeof(s_external_dir), "%s", path );
    s_sel_index = shader_external_index ();

    util_alloc_string ( &gcfg->video.shader, s_external_dir );
    s_shader_video_mask |= KCFG_VIDEO_SHADER;
    s_dirty = 1;

    shader_show_restart_warning_if_needed ( s_external_dir );
    shader_load_working ( s_external_dir );
    guitop_show_toast_msg ( "Bundle loaded for preview." );
}

static void shader_load_dir ( struct option_widget *wgt, void *data )
{
    struct modal_config *cfg = guitop_get_modal_cfg ();

    (void) wgt;
    (void) data;

    memset ( cfg, 0, sizeof(struct modal_config) );
    cfg->modal_type = MODAL_FB_DIR;
    cfg->modal_active = 2;
    cfg->msg_type = 0;
    strcpy ( cfg->title, "Choose Shader Bundle Directory" );
    cfg->post_show = shader_dir_result;

    filebrowser_init ( cfg );
}

/* ---- runtime reload after save ---- */

static void shader_apply_runtime ( void )
{
    t_config *cfg = config_get_global_cfg ();
    const char *sel = cfg->video.shader;
    int want = sel && strlen(sel) && strcmp(sel, SHADER_NONE);

    if ( !pc_get_gpu_device() ) {
        guitop_show_toast_msg ( want
                ? "Saved. Live shader needs the GPU renderer (restart)."
                : "Saved." );
        return;
    }

    /* Rebuild the gameplay shader runtime from the freshly saved config.  On
       failure pc_set_shader leaves the normal renderer path active, so the GUI
       and gameplay stay usable and the next launch retries from the config. */
    pc_set_shader ( &cfg->video );

    if ( !want )
        guitop_show_toast_msg ( "Shader disabled." );
    else if ( g_pc_if.use_shader )
        guitop_show_toast_msg ( "Shader saved and active." );
    else
        guitop_show_toast_msg ( "Saved. Shader will apply after restart." );
}

static void shader_save_all ( struct option_widget *wgt, void *data )
{
    (void) wgt;
    (void) data;

    config_save_video ( config_get_global_table(), "All", KCFG_VIDEO_SHADER );
    if ( s_bundle_loaded )
        config_shader_bundle_info ( config_get_global_table(), &s_bundle,
                KCFG_SAVE );

    s_shader_video_mask = 0;
    s_dirty = 0;
    config_refresh_rom_source_masks ();

    shader_apply_runtime ();
}

static void shader_save_rom ( struct option_widget *wgt, void *data )
{
    (void) wgt;
    (void) data;

    if ( !config_ensure_rom_table() ) {
        guitop_show_toast_msg ( "No ROM loaded." );
        return;
    }

    config_save_video ( config_get_rom_table(), "this", KCFG_VIDEO_SHADER );
    if ( s_bundle_loaded )
        config_shader_bundle_info ( config_get_rom_table(), &s_bundle,
                KCFG_SAVE );

    config_refresh_rom_source_masks ();
    s_shader_video_mask = 0;
    s_dirty = 0;

    /* Reload the working copy so per-param ROM markers reflect what was saved. */
    shader_load_working ( config_get_global_cfg()->video.shader );

    shader_apply_runtime ();
}

/* ---- enter / exit ---- */

void menu_shader_enter ( void )
{
    t_config *cfg = config_get_global_cfg ();
    const char *cur = cfg->video.shader;

    for ( int i = 0; i < s_bundle_count; ++i ) {
        free ( s_bundle_ids[i] );
        s_bundle_ids[i] = NULL;
    }
    s_bundle_count = pc_enumerate_shader_bundles ( s_bundle_ids,
            SHADER_MAX_BUNDLES );

    s_has_external = 0;
    s_external_dir[0] = '\0';
    s_sel_index = 0;
    s_restart_warning_shown = 0;

    if ( cur && strlen(cur) && strcmp(cur, SHADER_NONE) ) {
        int found = -1;
        for ( int i = 0; i < s_bundle_count; ++i ) {
            if ( !strcmp(cur, s_bundle_ids[i]) ) {
                found = i;
                break;
            }
        }
        if ( found >= 0 ) {
            s_sel_index = found + 1;
        }
        else {
            char dir[KAT_PATH_MAX];
            s_has_external = 1;
            snprintf ( s_external_dir, sizeof(s_external_dir), "%s", cur );
            /* Only make the external slot the active selection if it actually
               resolves.  A stale path stays listed (so the user can see and
               move to it) but the active selection shows None. */
            if ( pc_resolve_shader_bundle ( cur, dir, sizeof(dir) ) == 0 )
                s_sel_index = shader_external_index ();
            else
                s_sel_index = 0;
        }
    }

    shader_load_working ( cur );

    s_shader_video_mask = 0;
    s_dirty = 0;
    s_initialized = 1;
}

void menu_shader_exit ( void )
{
    pc_shader_preview_free ();
    shader_bundle_free ( &s_bundle );
    s_bundle_loaded = 0;
    s_param_count = 0;
    shader_reset_groups ();

    for ( int i = 0; i < s_bundle_count; ++i ) {
        free ( s_bundle_ids[i] );
        s_bundle_ids[i] = NULL;
    }
    s_bundle_count = 0;

    if ( s_cpu_tex ) {
        SDL_DestroyTexture ( s_cpu_tex );
        s_cpu_tex = NULL;
        s_cpu_w = 0;
        s_cpu_h = 0;
    }

    s_initialized = 0;
}

/* ---- menu builder ---- */

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

    if ( !s_initialized )
        menu_shader_enter ();

    for ( i = 0; i < MAX_OPTIONS; i++ ) {
        wid_ary[i].widget_type = WIDGET_TYPE_LABEL;
        wid_ary[i].data_type = WIDGET_DATA_TYPE_INT;
        wid_ary[i].menu_prev = KAT_MENU_OPTIONS_VIDEO;
        wid_ary[i].menu_next = 0;
        wid_ary[i].divider = 0;
        wid_ary[i].data = NULL;
        wid_ary[i].mask_value = 0;
        wid_ary[i].p_mask = NULL;
        wid_ary[i].p_source_mask = NULL;
        wid_ary[i].get_string = NULL;
        wid_ary[i].run_task = NULL;
    }

    i = -1;

    /* Shader selection: None + shipped bundles (+ external slot) */
    wid_ary[++i].widget_type = WIDGET_TYPE_SELECT;
    strcpy ( wid_ary[i].label, "Shader" );
    wid_ary[i].min = 0;
    wid_ary[i].max = shader_option_count() - 1;
    wid_ary[i].data = &s_sel_index;
    wid_ary[i].p_mask = &s_shader_video_mask;
    wid_ary[i].mask_value = KCFG_VIDEO_SHADER;
    wid_ary[i].p_source_mask = &cfg->video_mask;
    wid_ary[i].get_string = shader_sel_string;
    wid_ary[i].run_task = shader_sel_changed;

    /* Load an external precompiled bundle directory */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy ( wid_ary[i].label, "Load Bundle Directory" );
    wid_ary[i].run_task = shader_load_dir;

    if ( s_bundle_loaded && s_param_count > 0 ) {

        /* Reset all parameters to bundle defaults */
        wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
        strcpy ( wid_ary[i].label, "Reset Parameters" );
        wid_ary[i].divider = 1;
        wid_ary[i].run_task = shader_reset_params;

        for ( int k = 0; k < s_param_count && i < MAX_OPTIONS - 4; ++k ) {
            if ( shader_param_group_bucketed(s_param_stage[k].param) )
                continue;
            if ( !shader_append_param_widget ( wid_ary, &i, k ) )
                break;
        }

        for ( int g = 0; g < s_group_count && i < MAX_OPTIONS - 4; ++g ) {
            if ( !shader_append_group_header ( wid_ary, &i, g ) )
                break;
            if ( s_groups[g].collapsed )
                continue;

            for ( int k = 0; k < s_param_count && i < MAX_OPTIONS - 4; ++k ) {
                if ( !shader_group_name_matches ( s_param_stage[k].param,
                            s_groups[g].name ) )
                    continue;
                if ( !shader_append_param_widget ( wid_ary, &i, k ) )
                    break;
            }
        }
    }

    /* Save */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy ( wid_ary[i].label, "  Save" );
    wid_ary[i].divider = 1;
    wid_ary[i].run_task = shader_save_all;

    /* Save for ROM only */
    wid_ary[++i].widget_type = WIDGET_TYPE_LABEL;
    strcpy ( wid_ary[i].label, "  Save for ROM only [*]" );
    wid_ary[i].run_task = shader_save_rom;

    return i + 1;
}

/* ---- preview ---- */

static void shader_preview_draw_image ( struct nk_context *ctx,
        SDL_Texture *tex, int img_w, int img_h )
{
    struct nk_vec2 avail = nk_window_get_content_region_size ( ctx );
    float fit_w = avail.x - PREVIEW_RIGHT_INSET;

    if ( img_w <= 0 || img_h <= 0 )
        return;
    if ( fit_w < img_w )
        fit_w = (float) img_w;

    if ( avail.x >= img_w && avail.y >= img_h ) {
        float scale = ( fit_w / img_w < avail.y / img_h )
                      ? fit_w / img_w : avail.y / img_h;
        int draw_w = (int)( img_w * scale );
        int draw_h = (int)( img_h * scale );
        nk_layout_row_static ( ctx, (float) draw_h, draw_w, 1 );
        nk_image ( ctx, nk_image_ptr((void *) tex) );
    }
    else {
        nk_layout_row_dynamic ( ctx, avail.y, 1 );
        if ( nk_group_begin(ctx, "ShaderPreviewContent", 0) ) {
            nk_layout_row_static ( ctx, (float) img_h, img_w, 1 );
            nk_image ( ctx, nk_image_ptr((void *) tex) );
            nk_group_end ( ctx );
        }
    }
}

static void shader_preview_draw_cpu_fallback ( struct nk_context *ctx )
{
    int img_w = g_pc_if.draw_video_rect.w;
    int img_h = g_pc_if.draw_video_rect.h;

    if ( !g_pc_if.surface || !g_pc_if.draw_pixel_start || img_w <= 0 ||
            img_h <= 0 ) {
        nk_layout_row_dynamic ( ctx, 20, 1 );
        nk_label ( ctx, "No video", NK_TEXT_CENTERED );
        return;
    }

    if ( s_cpu_tex && ( s_cpu_w != img_w || s_cpu_h != img_h ) ) {
        SDL_DestroyTexture ( s_cpu_tex );
        s_cpu_tex = NULL;
    }
    if ( !s_cpu_tex ) {
        s_cpu_tex = SDL_CreateTexture ( g_pc_if.renderer,
                SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING,
                img_w, img_h );
        if ( s_cpu_tex ) {
            SDL_SetTextureBlendMode ( s_cpu_tex, SDL_BLENDMODE_NONE );
            s_cpu_w = img_w;
            s_cpu_h = img_h;
        }
    }

    if ( s_cpu_tex ) {
        SDL_UpdateTexture ( s_cpu_tex, NULL, g_pc_if.draw_pixel_start,
                g_pc_if.draw_pitch );
        shader_preview_draw_image ( ctx, s_cpu_tex, img_w, img_h );
    }
}

void menu_shader_draw_preview ( struct nk_context *ctx, int x, int y, int w,
        int h )
{
    nk_flags flags = NK_WINDOW_NO_SCROLLBAR;
    char live_status[128];

    if ( nk_begin(ctx, "ShaderPreview",
                nk_rect((float)x, (float)y, (float)w, (float)h), flags) ) {

        pc_get_shader_live_status ( live_status, sizeof(live_status) );

        if ( !s_bundle_loaded ) {
            nk_layout_row_dynamic ( ctx, 20, 1 );
            nk_label ( ctx, s_status[0] ? s_status : "Shader disabled.",
                    NK_TEXT_CENTERED );
            if ( live_status[0] ) {
                nk_layout_row_dynamic ( ctx, 18, 1 );
                nk_label ( ctx, live_status, NK_TEXT_CENTERED );
            }
            shader_preview_draw_cpu_fallback ( ctx );
        }
        else {
            char err[256];
            SDL_Texture *tex = pc_shader_preview_update ( &s_bundle, err,
                    sizeof(err) );

            if ( tex ) {
                nk_layout_row_dynamic ( ctx, 18, 1 );
                nk_label ( ctx, s_status, NK_TEXT_CENTERED );
                if ( live_status[0] ) {
                    nk_layout_row_dynamic ( ctx, 18, 1 );
                    nk_label ( ctx, live_status, NK_TEXT_CENTERED );
                }
                shader_preview_draw_image ( ctx, tex,
                        g_pc_if.draw_video_rect.w, g_pc_if.draw_video_rect.h );
            }
            else {
                nk_layout_row_dynamic ( ctx, 18, 1 );
                nk_label ( ctx, s_status, NK_TEXT_CENTERED );
                if ( live_status[0] ) {
                    nk_layout_row_dynamic ( ctx, 18, 1 );
                    nk_label ( ctx, live_status, NK_TEXT_CENTERED );
                }
                nk_layout_row_dynamic ( ctx, 18, 1 );
                nk_label ( ctx,
                        err[0] ? err
                        : "Live shader preview unavailable; showing unshaded output.",
                        NK_TEXT_CENTERED );
                shader_preview_draw_cpu_fallback ( ctx );
            }
        }
    }
    nk_end ( ctx );
}
