// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

// ctrlpanel.c  -- DVX Control Panel
//
// System configuration app with four tabs:
//   Mouse     -- scroll direction, wheel speed, double-click speed, acceleration, cursor speed
//   Colors    -- all 18 system colors, theme load/save
//   Desktop   -- wallpaper image (stretch mode)
//   Video     -- resolution and color depth
//
// Changes preview live.  OK saves to DVX.INI, Cancel reverts to the
// state captured when the control panel was opened.

#include "dvxApp.h"
#include "dvxDlg.h"
#include "dvxPrefs.h"
#include "dvxWgt.h"
#include "box/box.h"
#include "button/button.h"
#include "canvas/canvas.h"
#include "dropdown/dropdown.h"
#include "label/label.h"
#include "listBox/listBox.h"
#include "slider/slider.h"
#include "spacer/spacer.h"
#include "tabControl/tabCtrl.h"
#include "dvxWm.h"
#include "dvxPlat.h"
#include "shellApp.h"
#include "stb_ds_wrap.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include "dvxMem.h"

// ============================================================
// Constants
// ============================================================

#define CP_WIN_W           460
#define CP_WIN_H           340
#define CP_SPACING         8
#define CP_SPACING_SMALL   4
#define CP_THEME_BTN_W     60
#define CP_SAVE_BTN_W      70
#define CP_SWATCH_W        64
#define CP_SWATCH_H        24
#define CP_DBLCLICK_LBL_W  52
#define CP_TEST_BTN_W      100
#define CP_APPLY_BTN_W     100
#define CP_OK_CANCEL_BTN_W 80
#define CP_CONFIRM_W       300
#define CP_CONFIRM_H       100
#define CP_CONFIRM_BTN_W   70
#define CP_WPAPER_BTN_W    90
// Cursor-speed slider runs slow..fast (low..high), but the platform
// mickey-to-pixel ratio runs the opposite way (high = slow).  The two
// are mirror images across the slider's endpoints, so the conversion
// in both directions is: other = (MIN + MAX) - this.  The INI 'speed'
// key and the boot path store/consume the mickey ratio (see shellMain).
#define CP_SPEED_SLIDER_MIN  2
#define CP_SPEED_SLIDER_MAX  32
#define CP_SPEED_INVERT      (CP_SPEED_SLIDER_MIN + CP_SPEED_SLIDER_MAX)
#define WPAPER_DIR         "CONFIG/WPAPER"
#define THEME_DIR        "CONFIG/THEMES"
#define THEME_EXT        ".THM"

// ============================================================
// Entry types for dynamic lists
// ============================================================

typedef struct {
    char name[64];
    char path[DVX_MAX_PATH];
} FileEntryT;

// ============================================================
// Module state
// ============================================================

static AppContextT    *sAc    = NULL;
static WindowT        *sWin   = NULL;
static PrefsHandleT  *sPrefs  = NULL;

// Saved state for Cancel
static uint8_t  sSavedColorRgb[ColorCountE][3];
static int32_t  sSavedWheelDir;
static int32_t  sSavedDblClick;
static int32_t  sSavedAccelThresh;   // accel threshold (dvxSetMouseConfig input)
static int32_t  sSavedMickeys;       // mickey ratio (dvxSetMouseConfig input)
static int32_t  sSavedWheelStep;
static int32_t  sSavedVideoW;
static int32_t  sSavedVideoH;
static int32_t       sSavedVideoBpp;
static WallpaperModeE sSavedWpMode;
static char sSavedWallpaperPath[DVX_MAX_PATH];

// Mouse tab widgets
static WidgetT *sWheelDrop     = NULL;
static WidgetT *sDblClickSldr  = NULL;
static WidgetT *sDblClickLbl   = NULL;
static WidgetT *sAccelDrop     = NULL;
static WidgetT *sSpeedSldr     = NULL;
static WidgetT *sSpeedLbl      = NULL;
static WidgetT *sWheelStepSldr = NULL;
static WidgetT *sWheelStepLbl  = NULL;
static WidgetT *sDblTestLbl    = NULL;

// Colors tab widgets
static WidgetT *sColorList     = NULL;
static WidgetT *sRedSldr       = NULL;
static WidgetT *sGreenSldr     = NULL;
static WidgetT *sBlueSldr      = NULL;
static WidgetT *sRedLbl        = NULL;
static WidgetT *sGreenLbl      = NULL;
static WidgetT *sBlueLbl       = NULL;
static WidgetT *sColorSwatch   = NULL;

// Desktop tab widgets
static WidgetT         *sWallpaperLbl  = NULL;
static WidgetT         *sWpaperList    = NULL;
static WidgetT         *sWpModeDrop    = NULL;
static char             sWallpaperPath[DVX_MAX_PATH];
static FileEntryT      *sWpaperEntries = NULL;  // stb_ds dynamic array
static const char     **sWpaperLabels  = NULL;  // stb_ds dynamic array

// Video tab
static const VideoModeInfoT *sVideoModes = NULL;  // points into ctx, not owned
static int32_t               sVideoCount = 0;
static const char          **sVideoLabels = NULL;  // stb_ds dynamic array
static char                (*sLabelBufs)[48] = NULL;  // stb_ds dynamic array of mode labels
static WidgetT              *sVideoList   = NULL;
static int32_t sVideoConfirmResult = -1;

// Theme list
static FileEntryT      *sThemeEntries  = NULL;  // stb_ds dynamic array
static const char     **sThemeLabels   = NULL;  // stb_ds dynamic array
static WidgetT         *sThemeList     = NULL;

// ============================================================
// Prototypes
// ============================================================

int32_t     appMain(DxeAppContextT *ctx);
static void applyMouseConfig(void);
static void buildColorsTab(WidgetT *page);
static void buildDesktopTab(WidgetT *page);
static void buildMouseTab(WidgetT *page);
static void buildVideoTab(WidgetT *page);
static int32_t mapAccelName(const char *name);
static const char *mapAccelValue(int32_t val);
static void onAccelChange(WidgetT *w);
static void onApplyTheme(WidgetT *w);
static void onApplyWallpaper(WidgetT *w);
static void onBrowseTheme(WidgetT *w);
static void onCancel(WidgetT *w);
static void onChooseWallpaper(WidgetT *w);
static void onClearWallpaper(WidgetT *w);
static void onClose(WindowT *win);
static void onColorSelect(WidgetT *w);
static void onColorSlider(WidgetT *w);
static void onDblClickSlider(WidgetT *w);
static void onDblClickTest(WidgetT *w);
static void onOk(WidgetT *w);
static void onResetColors(WidgetT *w);
static void onSaveTheme(WidgetT *w);
static void onSpeedSlider(WidgetT *w);
static void onVideoApply(WidgetT *w);
static void onVideoConfirmClose(WindowT *win);
static void onVideoConfirmNo(WidgetT *w);
static void onVideoConfirmYes(WidgetT *w);
static void onWallpaperMode(WidgetT *w);
static void onWheelChange(WidgetT *w);
static void onWheelStepSlider(WidgetT *w);
static void restoreSnapshot(void);
static void saveSnapshot(void);
static void scanThemes(void);
static void scanWallpapers(void);
static void updateColorSliders(void);
static void updateDblClickLabel(void);
static void updateSpeedLabel(void);
static void updateSwatch(void);
static void updateWheelStepLabel(void);


// ============================================================
// App descriptor
// ============================================================

AppDescriptorT appDescriptor = {
    .name          = "Control Panel",
    .hasMainLoop   = false,
    .multiInstance = false,
    .stackSize     = SHELL_STACK_DEFAULT,
    .priority      = TS_PRIORITY_NORMAL
};


// Cleanup hook the shell calls before dlclose: these module-static stb_ds
// arrays (and any still-open prefs handle) would otherwise leak into the
// long-lived shell heap on every open/close, since the DXE unload doesn't
// free tracked blocks.
void appShutdown(void) {
    arrfree(sThemeEntries);
    sThemeEntries = NULL;
    arrfree(sThemeLabels);
    sThemeLabels = NULL;
    arrfree(sWpaperEntries);
    sWpaperEntries = NULL;
    arrfree(sWpaperLabels);
    sWpaperLabels = NULL;
    arrfree(sVideoLabels);
    sVideoLabels = NULL;
    arrfree(sLabelBufs);
    sLabelBufs = NULL;

    if (sPrefs) {
        prefsClose(sPrefs);
        sPrefs = NULL;
    }
}


static void applyMouseConfig(void) {
    int32_t dir = (wgtDropdownGetSelected(sWheelDrop) == 1) ? -1 : 1;
    int32_t dbl = wgtSliderGetValue(sDblClickSldr);

    const char *accelName = mapAccelValue(wgtDropdownGetSelected(sAccelDrop));
    int32_t     accelVal  = mapAccelName(accelName);

    // Slider is inverted vs the mickey ratio (see CP_SPEED_INVERT):
    //   slider 2  -> 32 mickeys/8px (slowest)
    //   slider 26 -> 8  mickeys/8px (default)
    //   slider 32 -> 2  mickeys/8px (fastest)
    int32_t speedVal  = CP_SPEED_INVERT - wgtSliderGetValue(sSpeedSldr);
    int32_t wheelStep = wgtSliderGetValue(sWheelStepSldr);

    dvxSetMouseConfig(sAc, dir, dbl, accelVal, speedVal, wheelStep);
}


static void buildColorsTab(WidgetT *page) {
    // Color list on the left, sliders on the right
    WidgetT *hbox = wgtHBox(page);
    hbox->weight  = WGT_WEIGHT_FILL;
    hbox->spacing = wgtPixels(CP_SPACING);

    // Left side: color name list + theme controls
    WidgetT *leftVbox = wgtVBox(hbox);
    leftVbox->weight  = 50;
    leftVbox->spacing = wgtPixels(CP_SPACING_SMALL);

    wgtLabel(leftVbox, "System Colors:");

    // Build color name list
    static const char *colorNames[ColorCountE];

    for (int32_t i = 0; i < ColorCountE; i++) {
        colorNames[i] = dvxColorLabel((ColorIdE)i);
    }

    sColorList = wgtListBox(leftVbox);
    sColorList->weight   = WGT_WEIGHT_FILL;
    sColorList->onChange  = onColorSelect;
    wgtListBoxSetItems(sColorList, colorNames, ColorCountE);
    wgtListBoxSetSelected(sColorList, 0);

    // Theme row
    WidgetT *themeRow = wgtHBox(leftVbox);
    themeRow->spacing = wgtPixels(CP_SPACING_SMALL);

    wgtLabel(themeRow, "Theme:");

    scanThemes();
    sThemeList = wgtDropdown(themeRow);
    sThemeList->weight = WGT_WEIGHT_FILL;
    wgtDropdownSetItems(sThemeList, sThemeLabels, arrlen(sThemeEntries));

    WidgetT *loadBtn = wgtButton(themeRow, "Apply");
    loadBtn->onClick = onApplyTheme;
    loadBtn->prefW   = wgtPixels(CP_THEME_BTN_W);

    WidgetT *browseBtn = wgtButton(themeRow, "Load...");
    browseBtn->onClick = onBrowseTheme;
    browseBtn->prefW   = wgtPixels(CP_THEME_BTN_W);

    WidgetT *saveBtn = wgtButton(themeRow, "Save As...");
    saveBtn->onClick = onSaveTheme;
    saveBtn->prefW   = wgtPixels(CP_SAVE_BTN_W);

    WidgetT *resetBtn = wgtButton(themeRow, "Reset");
    resetBtn->onClick = onResetColors;
    resetBtn->prefW   = wgtPixels(CP_THEME_BTN_W);

    // Right side: RGB sliders in a non-weighted inner box so they stay
    // at natural size.  The outer box absorbs extra vertical space.
    WidgetT *rightVbox = wgtVBox(hbox);
    rightVbox->weight  = 50;

    WidgetT *sliderBox = wgtVBox(rightVbox);
    sliderBox->spacing = wgtPixels(CP_SPACING_SMALL);

    wgtLabel(sliderBox, "Red:");
    sRedSldr = wgtSlider(sliderBox, 0, 255);
    sRedSldr->onChange = onColorSlider;
    sRedLbl = wgtLabel(sliderBox, "0");
    wgtLabelSetAlign(sRedLbl, AlignEndE);

    wgtLabel(sliderBox, "Green:");
    sGreenSldr = wgtSlider(sliderBox, 0, 255);
    sGreenSldr->onChange = onColorSlider;
    sGreenLbl = wgtLabel(sliderBox, "0");
    wgtLabelSetAlign(sGreenLbl, AlignEndE);

    wgtLabel(sliderBox, "Blue:");
    sBlueSldr = wgtSlider(sliderBox, 0, 255);
    sBlueSldr->onChange = onColorSlider;
    sBlueLbl = wgtLabel(sliderBox, "0");
    wgtLabelSetAlign(sBlueLbl, AlignEndE);

    wgtLabel(sliderBox, "Preview:");
    sColorSwatch = wgtCanvas(sliderBox, CP_SWATCH_W, CP_SWATCH_H);

    // Absorb remaining vertical space below the sliders
    wgtSpacer(rightVbox)->weight = WGT_WEIGHT_FILL;

    updateColorSliders();
}


static void buildDesktopTab(WidgetT *page) {
    wgtLabel(page, "Wallpaper:");

    scanWallpapers();
    sWpaperList = wgtListBox(page);
    sWpaperList->weight     = WGT_WEIGHT_FILL;
    sWpaperList->onDblClick = onApplyWallpaper;
    wgtListBoxSetItems(sWpaperList, sWpaperLabels, arrlen(sWpaperEntries));

    sWallpaperLbl = wgtLabel(page, sWallpaperPath[0] ? sWallpaperPath : "(none)");

    WidgetT *btnRow = wgtHBox(page);
    btnRow->spacing = wgtPixels(CP_SPACING);

    WidgetT *applyBtn = wgtButton(btnRow, "Apply");
    applyBtn->onClick = onApplyWallpaper;
    applyBtn->prefW   = wgtPixels(CP_WPAPER_BTN_W);

    WidgetT *chooseBtn = wgtButton(btnRow, "Browse...");
    chooseBtn->onClick = onChooseWallpaper;
    chooseBtn->prefW   = wgtPixels(CP_WPAPER_BTN_W);

    WidgetT *clearBtn = wgtButton(btnRow, "Clear");
    clearBtn->onClick  = onClearWallpaper;
    clearBtn->prefW    = wgtPixels(CP_WPAPER_BTN_W);

    wgtSpacer(btnRow)->weight = WGT_WEIGHT_FILL;
    wgtLabel(btnRow, "Mode:");

    static const char *modeItems[] = {"Stretch", "Tile", "Center"};
    sWpModeDrop = wgtDropdown(btnRow);
    sWpModeDrop->onChange = onWallpaperMode;
    wgtDropdownSetItems(sWpModeDrop, modeItems, 3);
    wgtDropdownSetSelected(sWpModeDrop, (int32_t)sAc->wallpaperMode);
}


static void buildMouseTab(WidgetT *page) {
    page->spacing = wgtPixels(6);

    // Scroll direction
    WidgetT *wheelRow = wgtHBox(page);
    wheelRow->spacing = wgtPixels(CP_SPACING);
    wgtLabel(wheelRow, "Scroll Wheel:");

    static const char *wheelItems[] = {"Normal", "Reversed"};
    sWheelDrop = wgtDropdown(wheelRow);
    sWheelDrop->weight  = WGT_WEIGHT_FILL;
    sWheelDrop->onChange = onWheelChange;
    wgtDropdownSetItems(sWheelDrop, wheelItems, 2);
    wgtDropdownSetSelected(sWheelDrop, sAc->wheelDirection < 0 ? 1 : 0);

    wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);

    // Wheel speed (lines per notch)
    wgtLabel(page, "Scroll Speed:");

    WidgetT *wheelStepRow = wgtHBox(page);
    wheelStepRow->spacing = wgtPixels(CP_SPACING);

    wgtLabel(wheelStepRow, "Slow");
    sWheelStepSldr = wgtSlider(wheelStepRow, MOUSE_WHEEL_STEP_MIN, MOUSE_WHEEL_STEP_MAX);
    sWheelStepSldr->weight   = WGT_WEIGHT_FILL;
    sWheelStepSldr->onChange  = onWheelStepSlider;

    int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);
    wgtSliderSetValue(sWheelStepSldr, wheelStep);
    wgtLabel(wheelStepRow, "Fast  ");
    sWheelStepLbl = wgtLabel(wheelStepRow, "");
    sWheelStepLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
    updateWheelStepLabel();

    wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);

    // Double-click speed
    wgtLabel(page, "Double-Click Speed:");

    WidgetT *dblRow = wgtHBox(page);
    dblRow->spacing = wgtPixels(CP_SPACING);

    wgtLabel(dblRow, "Fast");
    sDblClickSldr = wgtSlider(dblRow, 200, 900);
    sDblClickSldr->weight   = WGT_WEIGHT_FILL;
    sDblClickSldr->onChange  = onDblClickSlider;

    int32_t dblMs = prefsGetInt(sPrefs, "mouse", "doubleclick", MOUSE_DBLCLICK_DEFAULT_MS);
    wgtSliderSetValue(sDblClickSldr, dblMs);
    wgtLabel(dblRow, "Slow  ");
    sDblClickLbl = wgtLabel(dblRow, "");
    sDblClickLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
    updateDblClickLabel();

    wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);

    // Acceleration
    WidgetT *accelRow = wgtHBox(page);
    accelRow->spacing = wgtPixels(CP_SPACING);
    wgtLabel(accelRow, "Acceleration:");

    static const char *accelItems[] = {"Off", "Low", "Medium", "High"};
    sAccelDrop = wgtDropdown(accelRow);
    sAccelDrop->weight  = WGT_WEIGHT_FILL;
    sAccelDrop->onChange = onAccelChange;
    wgtDropdownSetItems(sAccelDrop, accelItems, 4);

    const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", MOUSE_ACCEL_DEFAULT);

    if (strcmp(accelStr, "off") == 0) {
        wgtDropdownSetSelected(sAccelDrop, 0);
    } else if (strcmp(accelStr, "low") == 0) {
        wgtDropdownSetSelected(sAccelDrop, 1);
    } else if (strcmp(accelStr, "high") == 0) {
        wgtDropdownSetSelected(sAccelDrop, 3);
    } else {
        wgtDropdownSetSelected(sAccelDrop, 2);
    }

    wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);

    // Speed (mickey-to-pixel ratio)
    wgtLabel(page, "Cursor Speed:");

    WidgetT *speedRow = wgtHBox(page);
    speedRow->spacing = wgtPixels(CP_SPACING);

    wgtLabel(speedRow, "Slow");
    sSpeedSldr = wgtSlider(speedRow, CP_SPEED_SLIDER_MIN, CP_SPEED_SLIDER_MAX);
    sSpeedSldr->weight  = WGT_WEIGHT_FILL;
    sSpeedSldr->onChange = onSpeedSlider;

    // INI 'speed' holds the mickey ratio (the unit the platform and the
    // shell boot path use); convert to the inverted slider position.
    int32_t mickeys = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
    wgtSliderSetValue(sSpeedSldr, CP_SPEED_INVERT - mickeys);
    wgtLabel(speedRow, "Fast  ");
    sSpeedLbl = wgtLabel(speedRow, "");
    sSpeedLbl->prefW = wgtPixels(CP_DBLCLICK_LBL_W);
    updateSpeedLabel();

    wgtSpacer(page)->prefH = wgtPixels(CP_SPACING_SMALL);

    // Double-click test area
    WidgetT *testRow = wgtHBox(page);
    testRow->spacing = wgtPixels(CP_SPACING);

    WidgetT *testBtn = wgtButton(testRow, "Test Area");
    testBtn->onDblClick = onDblClickTest;
    testBtn->prefW      = wgtPixels(CP_TEST_BTN_W);

    sDblTestLbl = wgtLabel(testRow, "Double-click the button to test");
}


static void buildVideoTab(WidgetT *page) {
    wgtLabel(page, "Available Video Modes:");

    // Read modes enumerated at init time
    sVideoModes = dvxGetVideoModes(sAc, &sVideoCount);

    // Build label strings
    arrsetlen(sVideoLabels, 0);
    arrsetlen(sLabelBufs, sVideoCount);

    for (int32_t i = 0; i < sVideoCount; i++) {
        const char *depthName;

        switch (sVideoModes[i].bpp) {
            case 8:  depthName = "256 colors";          break;
            case 15: depthName = "32 thousand colors";   break;
            case 16: depthName = "65 thousand colors";   break;
            case 24: depthName = "16 million colors";    break;
            case 32: depthName = "16 million colors+";   break;
            default: depthName = "";                     break;
        }

        snprintf(sLabelBufs[i], sizeof(sLabelBufs[i]), "%ldx%ld %s", (long)sVideoModes[i].w, (long)sVideoModes[i].h, depthName);
        arrput(sVideoLabels, (const char *)sLabelBufs[i]);
    }

    sVideoList = wgtListBox(page);
    sVideoList->weight     = WGT_WEIGHT_FILL;
    sVideoList->onDblClick = onVideoApply;
    wgtListBoxSetItems(sVideoList, sVideoLabels, sVideoCount);

    // Select the current mode
    for (int32_t i = 0; i < sVideoCount; i++) {
        if (sVideoModes[i].w == sAc->display.width &&
            sVideoModes[i].h == sAc->display.height &&
            sVideoModes[i].bpp == sAc->display.format.bitsPerPixel) {
            wgtListBoxSetSelected(sVideoList, i);
            break;
        }
    }

    WidgetT *applyBtn = wgtButton(page, "Apply Mode");
    applyBtn->onClick = onVideoApply;
    applyBtn->prefW   = wgtPixels(CP_APPLY_BTN_W);
}


static int32_t mapAccelName(const char *name) {
    if (strcmp(name, "off") == 0) {
        return MOUSE_ACCEL_OFF;
    }
    if (strcmp(name, "low") == 0) {
        return MOUSE_ACCEL_LOW;
    }
    if (strcmp(name, "medium") == 0) {
        return MOUSE_ACCEL_MEDIUM;
    }
    if (strcmp(name, "high") == 0) {
        return MOUSE_ACCEL_HIGH;
    }
    return MOUSE_ACCEL_MEDIUM;
}


static const char *mapAccelValue(int32_t idx) {
    switch (idx) {
        case 0:  return "off";
        case 1:  return "low";
        case 2:  return "medium";
        case 3:  return "high";
        default: return "medium";
    }
}


static void onAccelChange(WidgetT *w) {
    (void)w;
    applyMouseConfig();
}


static void onApplyTheme(WidgetT *w) {
    (void)w;

    int32_t sel = wgtDropdownGetSelected(sThemeList);

    if (sel < 0 || sel >= arrlen(sThemeEntries)) {
        return;
    }

    dvxLoadTheme(sAc, sThemeEntries[sel].path);
    updateColorSliders();
}


static void onApplyWallpaper(WidgetT *w) {
    (void)w;

    int32_t sel = wgtListBoxGetSelected(sWpaperList);

    if (sel < 0 || sel >= arrlen(sWpaperEntries)) {
        return;
    }

    if (dvxSetWallpaper(sAc, sWpaperEntries[sel].path)) {
        snprintf(sWallpaperPath, sizeof(sWallpaperPath), "%s", sWpaperEntries[sel].path);
        wgtSetText(sWallpaperLbl, sWallpaperPath);
    } else {
        dvxErrorBox(sAc, NULL, "Could not load wallpaper image.");
    }
}


static void onBrowseTheme(WidgetT *w) {
    (void)w;

    FileFilterT filters[] = {
        { "Theme Files (*.thm)" },
        { "All Files (*.*)" }
    };
    char path[DVX_MAX_PATH];

    if (dvxFileDialog(sAc, "Load Theme", FD_OPEN, THEME_DIR, filters, 2, path, sizeof(path))) {
        dvxLoadTheme(sAc, path);
        updateColorSliders();
    }
}


static void onCancel(WidgetT *w) {
    (void)w;
    restoreSnapshot();
    prefsClose(sPrefs);
    sPrefs = NULL;
    dvxDestroyWindow(sAc, sWin);
    sWin = NULL;
}


static void onChooseWallpaper(WidgetT *w) {
    (void)w;

    FileFilterT filters[] = {
        { "Images (*.bmp;*.png;*.jpg;*.gif)" },
        { "All Files (*.*)" }
    };
    char path[DVX_MAX_PATH];

    if (dvxFileDialog(sAc, "Choose Wallpaper", FD_OPEN, WPAPER_DIR, filters, 2, path, sizeof(path))) {
        if (dvxSetWallpaper(sAc, path)) {
            strncpy(sWallpaperPath, path, sizeof(sWallpaperPath) - 1);
            sWallpaperPath[sizeof(sWallpaperPath) - 1] = '\0';
            wgtSetText(sWallpaperLbl, sWallpaperPath);
        } else {
            dvxErrorBox(sAc, NULL, "Could not load wallpaper image.");
        }
    }
}


static void onClearWallpaper(WidgetT *w) {
    (void)w;
    dvxSetWallpaper(sAc, NULL);
    sWallpaperPath[0] = '\0';
    wgtSetText(sWallpaperLbl, "(none)");
}


static void onClose(WindowT *win) {
    restoreSnapshot();
    prefsClose(sPrefs);
    sPrefs = NULL;
    dvxDestroyWindow(sAc, win);
    sWin = NULL;
}


static void onColorSelect(WidgetT *w) {
    (void)w;
    updateColorSliders();
}


static void onColorSlider(WidgetT *w) {
    (void)w;

    int32_t sel = wgtListBoxGetSelected(sColorList);

    if (sel < 0 || sel >= ColorCountE) {
        return;
    }

    uint8_t r = (uint8_t)wgtSliderGetValue(sRedSldr);
    uint8_t g = (uint8_t)wgtSliderGetValue(sGreenSldr);
    uint8_t b = (uint8_t)wgtSliderGetValue(sBlueSldr);

    // Update label text
    static char rBuf[8];
    static char gBuf[8];
    static char bBuf[8];
    snprintf(rBuf, sizeof(rBuf), "%d", r);
    snprintf(gBuf, sizeof(gBuf), "%d", g);
    snprintf(bBuf, sizeof(bBuf), "%d", b);
    wgtSetText(sRedLbl, rBuf);
    wgtSetText(sGreenLbl, gBuf);
    wgtSetText(sBlueLbl, bBuf);

    dvxSetColor(sAc, (ColorIdE)sel, r, g, b);
    updateSwatch();
}


static void onDblClickSlider(WidgetT *w) {
    (void)w;
    updateDblClickLabel();
    applyMouseConfig();
}


static void onDblClickTest(WidgetT *w) {
    (void)w;
    static int32_t count = 0;
    count++;

    static char buf[48];
    snprintf(buf, sizeof(buf), "Double-click detected! (%ld)", (long)count);
    wgtSetText(sDblTestLbl, buf);
}


static void onOk(WidgetT *w) {
    (void)w;

    // Save mouse settings
    int32_t wheelSel = wgtDropdownGetSelected(sWheelDrop);
    prefsSetString(sPrefs, "mouse", "wheel", wheelSel == 1 ? "reversed" : MOUSE_WHEEL_DIR_DEFAULT);
    prefsSetInt(sPrefs, "mouse", "doubleclick", wgtSliderGetValue(sDblClickSldr));
    prefsSetString(sPrefs, "mouse", "acceleration", mapAccelValue(wgtDropdownGetSelected(sAccelDrop)));
    // Store the mickey ratio (not the raw slider position) so the boot
    // path, which applies this value directly, matches the live setting.
    prefsSetInt(sPrefs, "mouse", "speed", CP_SPEED_INVERT - wgtSliderGetValue(sSpeedSldr));
    prefsSetInt(sPrefs, "mouse", "wheelspeed", wgtSliderGetValue(sWheelStepSldr));

    // Save colors to INI
    for (int32_t i = 0; i < ColorCountE; i++) {
        uint8_t r;
        uint8_t g;
        uint8_t b;
        dvxGetColor(sAc, (ColorIdE)i, &r, &g, &b);

        char val[16];
        snprintf(val, sizeof(val), "%d,%d,%d", r, g, b);
        prefsSetString(sPrefs, "colors", dvxColorName((ColorIdE)i), val);
    }

    // Save desktop settings
    if (sWallpaperPath[0]) {
        prefsSetString(sPrefs, "desktop", "wallpaper", sWallpaperPath);
    } else {
        prefsRemove(sPrefs, "desktop", "wallpaper");
    }

    const char *modeStr = "stretch";

    if (sAc->wallpaperMode == WallpaperTileE) {
        modeStr = "tile";
    } else if (sAc->wallpaperMode == WallpaperCenterE) {
        modeStr = "center";
    }

    prefsSetString(sPrefs, "desktop", "mode", modeStr);

    // Save video settings
    prefsSetInt(sPrefs, "video", "width", sAc->display.width);
    prefsSetInt(sPrefs, "video", "height", sAc->display.height);
    prefsSetInt(sPrefs, "video", "bpp", sAc->display.format.bitsPerPixel);

    prefsSave(sPrefs);
    prefsClose(sPrefs);
    sPrefs = NULL;
    dvxDestroyWindow(sAc, sWin);
    sWin = NULL;
}


static void onResetColors(WidgetT *w) {
    (void)w;
    dvxResetColorScheme(sAc);
    updateColorSliders();
}


static void onSaveTheme(WidgetT *w) {
    (void)w;

    FileFilterT filters[] = {
        { "Theme Files (*.thm)" },
        { "All Files (*.*)" }
    };
    char path[DVX_MAX_PATH];

    if (dvxFileDialog(sAc, "Save Theme", FD_SAVE, THEME_DIR, filters, 2, path, sizeof(path))) {
        dvxSaveTheme(sAc, path);
        scanThemes();
        wgtDropdownSetItems(sThemeList, sThemeLabels, arrlen(sThemeEntries));
    }
}


static void onSpeedSlider(WidgetT *w) {
    (void)w;
    updateSpeedLabel();
    applyMouseConfig();
}


static void onVideoApply(WidgetT *w) {
    (void)w;

    // Guard against re-entrancy: the confirmation dialog runs a nested
    // dvxUpdate loop which can process pending events that call us again.
    static bool sInProgress = false;

    if (sInProgress) {
        return;
    }

    sInProgress = true;

    int32_t sel = wgtListBoxGetSelected(sVideoList);

    if (sel < 0 || sel >= sVideoCount) {
        sInProgress = false;
        return;
    }

    const VideoModeInfoT *m = &sVideoModes[sel];

    int32_t oldW   = sAc->display.width;
    int32_t oldH   = sAc->display.height;
    int32_t oldBpp = sAc->display.format.bitsPerPixel;

    if (m->w == oldW && m->h == oldH && m->bpp == oldBpp) {
        sInProgress = false;
        return;
    }

    if (dvxChangeVideoMode(sAc, m->w, m->h, m->bpp) != 0) {
        dvxErrorBox(sAc, NULL, "Failed to change video mode.");
        sInProgress = false;
        return;
    }

    // Confirmation with 10-second auto-revert countdown.  Run a nested
    // event loop that updates the dialog title each second.  If the user
    // clicks Yes the mode is kept; No or timeout reverts to the old mode.
    #define CONFIRM_SECONDS 10

    WindowT *confirmWin = dvxCreateWindowCentered(sAc, "Keep this mode?", CP_CONFIRM_W, CP_CONFIRM_H, false);

    if (!confirmWin) {
        dvxChangeVideoMode(sAc, oldW, oldH, oldBpp);
        sInProgress = false;
        return;
    }

    confirmWin->modal   = true;
    confirmWin->onClose = onVideoConfirmClose;
    sAc->modalWindow    = confirmWin;

    WidgetT *root = wgtInitWindow(sAc, confirmWin);
    root->spacing = wgtPixels(CP_SPACING);

    static char msgBuf[80];
    snprintf(msgBuf, sizeof(msgBuf), "Reverting in %d seconds...", CONFIRM_SECONDS);
    WidgetT *msgLbl = wgtLabel(root, msgBuf);

    WidgetT *btnRow = wgtHBox(root);
    btnRow->align   = AlignEndE;
    btnRow->spacing = wgtPixels(CP_SPACING);

    WidgetT *yesBtn = wgtButton(btnRow, "Yes");
    yesBtn->prefW   = wgtPixels(CP_CONFIRM_BTN_W);
    yesBtn->onClick = onVideoConfirmYes;

    WidgetT *noBtn = wgtButton(btnRow, "No");
    noBtn->prefW    = wgtPixels(CP_CONFIRM_BTN_W);
    noBtn->onClick  = onVideoConfirmNo;

    dvxFitWindow(sAc, confirmWin);

    sVideoConfirmResult = -1;
    clock_t lastSecond  = clock();
    int32_t secondsLeft = CONFIRM_SECONDS;

    while (sAc->running && secondsLeft > 0 && sVideoConfirmResult < 0) {
        dvxUpdate(sAc);

        clock_t now = clock();

        if ((now - lastSecond) >= CLOCKS_PER_SEC) {
            lastSecond = now;
            secondsLeft--;
            snprintf(msgBuf, sizeof(msgBuf), "Reverting in %ld seconds...", (long)secondsLeft);
            wgtSetText(msgLbl, msgBuf);
        }
    }

    sAc->modalWindow = NULL;
    dvxDestroyWindow(sAc, confirmWin);

    if (sVideoConfirmResult != 1) {
        dvxChangeVideoMode(sAc, oldW, oldH, oldBpp);
    }

    sInProgress = false;
}


static void onVideoConfirmClose(WindowT *win) {
    (void)win;
    sVideoConfirmResult = 0;
}


static void onVideoConfirmNo(WidgetT *w) {
    (void)w;
    sVideoConfirmResult = 0;
}


static void onVideoConfirmYes(WidgetT *w) {
    (void)w;
    sVideoConfirmResult = 1;
}


static void onWallpaperMode(WidgetT *w) {
    int32_t sel = wgtDropdownGetSelected(w);

    if (sel >= 0 && sel <= 2) {
        dvxSetWallpaperMode(sAc, (WallpaperModeE)sel);
    }
}


static void onWheelChange(WidgetT *w) {
    (void)w;
    applyMouseConfig();
}


static void onWheelStepSlider(WidgetT *w) {
    (void)w;
    updateWheelStepLabel();
    applyMouseConfig();
}


static void restoreSnapshot(void) {
    // Restore colors
    memcpy(sAc->colorRgb, sSavedColorRgb, sizeof(sSavedColorRgb));
    dvxApplyColorScheme(sAc);

    // Restore mouse to the pre-open state captured in saveSnapshot.
    // The snapshot already holds the dvxSetMouseConfig inputs directly.
    dvxSetMouseConfig(sAc, sSavedWheelDir, sSavedDblClick, sSavedAccelThresh, sSavedMickeys, sSavedWheelStep);

    // Restore video mode if changed
    if (sAc->display.width != sSavedVideoW ||
        sAc->display.height != sSavedVideoH ||
        sAc->display.format.bitsPerPixel != sSavedVideoBpp) {
        dvxChangeVideoMode(sAc, sSavedVideoW, sSavedVideoH, sSavedVideoBpp);
    }

    // Restore wallpaper only if path or mode changed
    if (strcmp(sAc->wallpaperPath, sSavedWallpaperPath) != 0 ||
        sAc->wallpaperMode != sSavedWpMode) {
        sAc->wallpaperMode = sSavedWpMode;

        if (sSavedWallpaperPath[0]) {
            dvxSetWallpaper(sAc, sSavedWallpaperPath);
        } else {
            dvxSetWallpaper(sAc, NULL);
        }
    }
}


static void saveSnapshot(void) {
    memcpy(sSavedColorRgb, sAc->colorRgb, sizeof(sSavedColorRgb));
    sSavedWheelDir      = sAc->wheelDirection;
    sSavedDblClick      = (int32_t)(sAc->dblClickTicks * 1000 / CLOCKS_PER_SEC);
    // Accel and mickey ratio are NOT cached on the AppContext, and the
    // mouse-tab widgets do not exist yet when saveSnapshot runs (it is
    // called before buildMouseTab).  Capture them from prefs, which hold
    // the currently-applied values -- reading the NULL widgets here used
    // to snapshot garbage (-1 / 0) and corrupt the mouse on Cancel.
    const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", MOUSE_ACCEL_DEFAULT);
    sSavedAccelThresh   = mapAccelName(accelStr);
    sSavedMickeys       = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
    sSavedWheelStep     = sAc->wheelStep;
    sSavedVideoW        = sAc->display.width;
    sSavedVideoH        = sAc->display.height;
    sSavedVideoBpp      = sAc->display.format.bitsPerPixel;

    // Save the active wallpaper path and mode from the context
    snprintf(sSavedWallpaperPath, sizeof(sSavedWallpaperPath), "%s", sAc->wallpaperPath);
    snprintf(sWallpaperPath, sizeof(sWallpaperPath), "%s", sAc->wallpaperPath);
    sSavedWpMode = sAc->wallpaperMode;
}


static void scanThemes(void) {
    arrsetlen(sThemeEntries, 0);
    arrsetlen(sThemeLabels, 0);

    char **names = dvxReadDir(THEME_DIR);

    if (!names) {
        return;
    }

    int32_t n = (int32_t)arrlen(names);

    for (int32_t i = 0; i < n; i++) {
        if (!dvxHasExt(names[i], THEME_EXT)) {
            continue;
        }

        FileEntryT    entry      = {0};
        int32_t       nameLen    = (int32_t)strlen(names[i]) - (int32_t)strlen(THEME_EXT);

        if (nameLen >= (int32_t)sizeof(entry.name)) {
            nameLen = (int32_t)sizeof(entry.name) - 1;
        }

        // entry.name is the extension-stripped label shown in the
        // dropdown.  entry.path must use the ORIGINAL filename (with
        // extension) so dvxLoadTheme can open it -- previously we
        // used entry.name here, which pointed at a path missing
        // .THEME and silently failed.
        memcpy(entry.name, names[i], nameLen);
        entry.name[nameLen] = '\0';
        snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", THEME_DIR, names[i]);
        arrput(sThemeEntries, entry);
    }

    dvxReadDirFree(names);

    // Build label array now that sThemeEntries is stable
    for (int32_t i = 0; i < arrlen(sThemeEntries); i++) {
        arrput(sThemeLabels, sThemeEntries[i].name);
    }
}


static void scanWallpapers(void) {
    arrsetlen(sWpaperEntries, 0);
    arrsetlen(sWpaperLabels, 0);

    char **names = dvxReadDir(WPAPER_DIR);

    if (!names) {
        return;
    }

    int32_t n = (int32_t)arrlen(names);

    for (int32_t i = 0; i < n; i++) {
        if (!dvxHasExt(names[i], ".BMP") &&
            !dvxHasExt(names[i], ".JPG") &&
            !dvxHasExt(names[i], ".PNG")) {
            continue;
        }

        FileEntryT entry = {0};

        // Length-clamped memcpy instead of strncpy/snprintf because GCC
        // warns about both when d_name (255) exceeds the buffer (64),
        // even though truncation is intentional and safe.
        int32_t nl = (int32_t)strlen(names[i]);

        if (nl >= (int32_t)sizeof(entry.name)) {
            nl = (int32_t)sizeof(entry.name) - 1;
        }

        memcpy(entry.name, names[i], nl);
        entry.name[nl] = '\0';
        snprintf(entry.path, sizeof(entry.path), "%s" DVX_PATH_SEP "%s", WPAPER_DIR, entry.name);
        arrput(sWpaperEntries, entry);
    }

    dvxReadDirFree(names);

    // Build label array now that sWpaperEntries is stable
    for (int32_t i = 0; i < arrlen(sWpaperEntries); i++) {
        arrput(sWpaperLabels, sWpaperEntries[i].name);
    }
}


static void updateColorSliders(void) {
    int32_t sel = wgtListBoxGetSelected(sColorList);

    if (sel < 0 || sel >= ColorCountE) {
        return;
    }

    uint8_t r;
    uint8_t g;
    uint8_t b;
    dvxGetColor(sAc, (ColorIdE)sel, &r, &g, &b);

    wgtSliderSetValue(sRedSldr, r);
    wgtSliderSetValue(sGreenSldr, g);
    wgtSliderSetValue(sBlueSldr, b);

    static char rBuf[8];
    static char gBuf[8];
    static char bBuf[8];
    snprintf(rBuf, sizeof(rBuf), "%d", r);
    snprintf(gBuf, sizeof(gBuf), "%d", g);
    snprintf(bBuf, sizeof(bBuf), "%d", b);
    wgtSetText(sRedLbl, rBuf);
    wgtSetText(sGreenLbl, gBuf);
    wgtSetText(sBlueLbl, bBuf);
    updateSwatch();
}


static void updateDblClickLabel(void) {
    static char buf[32];
    snprintf(buf, sizeof(buf), "%ld ms", (long)wgtSliderGetValue(sDblClickSldr));
    wgtSetText(sDblClickLbl, buf);
}


static void updateSpeedLabel(void) {
    static char buf[32];
    int32_t val = wgtSliderGetValue(sSpeedSldr);
    snprintf(buf, sizeof(buf), "%ld", (long)val);
    wgtSetText(sSpeedLbl, buf);
}


static void updateSwatch(void) {
    if (!sColorSwatch) {
        return;
    }

    uint8_t r = (uint8_t)wgtSliderGetValue(sRedSldr);
    uint8_t g = (uint8_t)wgtSliderGetValue(sGreenSldr);
    uint8_t b = (uint8_t)wgtSliderGetValue(sBlueSldr);

    uint32_t color = packColor(&sAc->display, r, g, b);
    wgtCanvasClear(sColorSwatch, color);
}


static void updateWheelStepLabel(void) {
    static char buf[32];
    int32_t val = wgtSliderGetValue(sWheelStepSldr);
    snprintf(buf, sizeof(buf), "%ld", (long)val);
    wgtSetText(sWheelStepLbl, buf);
}


int32_t appMain(DxeAppContextT *ctx) {
    sAc   = ctx->shellCtx;
    sPrefs = prefsLoad(DVX_INI_PATH);

    int32_t winX = (sAc->display.width - CP_WIN_W) / 2;
    int32_t winY = (sAc->display.height - CP_WIN_H) / 2;

    sWin = dvxCreateWindow(sAc, "Control Panel", winX, winY, CP_WIN_W, CP_WIN_H, true);

    if (!sWin) {
        prefsClose(sPrefs);
        sPrefs = NULL;
        return -1;
    }

    sWin->onClose = onClose;

    saveSnapshot();

    WidgetT *root = wgtInitWindow(sAc, sWin);

    // Tab control fills the window
    WidgetT *tabs = wgtTabControl(root);
    tabs->weight  = WGT_WEIGHT_FILL;

    WidgetT *mouseTab   = wgtTabPage(tabs, "Mouse");
    WidgetT *colorsTab  = wgtTabPage(tabs, "Colors");
    WidgetT *desktopTab = wgtTabPage(tabs, "Desktop");
    WidgetT *videoTab   = wgtTabPage(tabs, "Video");

    buildMouseTab(mouseTab);
    buildColorsTab(colorsTab);
    buildDesktopTab(desktopTab);
    buildVideoTab(videoTab);

    // OK / Cancel buttons at bottom
    WidgetT *btnRow = wgtHBox(root);
    btnRow->align   = AlignEndE;
    btnRow->spacing = wgtPixels(CP_SPACING);

    WidgetT *okBtn = wgtButton(btnRow, "OK");
    okBtn->onClick = onOk;
    okBtn->prefW   = wgtPixels(CP_OK_CANCEL_BTN_W);

    WidgetT *cancelBtn = wgtButton(btnRow, "Cancel");
    cancelBtn->onClick = onCancel;
    cancelBtn->prefW   = wgtPixels(CP_OK_CANCEL_BTN_W);

    dvxFitWindow(sAc, sWin);
    return 0;
}
