// shellMain.c  -- DVX Shell entry point and main loop
//
// Initializes the GUI, task system, DXE export table, and loads
// the desktop app.  Runs the cooperative main loop, yielding to
// app tasks and reaping terminated apps each frame.
//
// The main loop design (dvxUpdate + tsYield + reap + notify):
// Each iteration does four things:
// 1. dvxUpdate: processes input events, dispatches callbacks, composites
//    dirty rects, flushes to the LFB. This is the shell's primary job.
// 2. tsYield: gives CPU time to app tasks. Without this, main-loop apps
//    would never run because the shell task would monopolize the CPU.
// 3. shellReapApps: cleans up any apps that terminated during this frame
//    (either their task returned or their last window was closed).
// 4. desktopUpdate: notifies the desktop app (Program Manager) if any
//    apps were reaped, so it can refresh its task list.
//
// Crash recovery uses setjmp/longjmp:
// The platform layer installs signal handlers for SIGSEGV, SIGFPE, SIGILL
// via platformInstallCrashHandler(). If a crash occurs in an app task, the
// handler logs platform-specific diagnostics (register dump on DJGPP),
// then longjmps back to the setjmp point in main(). This works because
// longjmp restores the main task's stack frame regardless of which task
// was running. tsRecoverToMain() then fixes the scheduler's bookkeeping,
// the shell logs app-specific info, and the crashed app is killed.
// This gives the shell Windows 3.1-style fault tolerance  -- one bad app
// doesn't take down the whole system.

#include "shellApp.h"
#include "shellInfo.h"
#include "dvxDialog.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "dvxComp.h"
#include "dvxPrefs.h"
#include "dvxPlatform.h"
#include "stb_ds_wrap.h"

#include <stdarg.h>
#include <setjmp.h>
#include <stdio.h>
#include <string.h>
#include "dvxMem.h"

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

static AppContextT  sCtx;
static PrefsHandleT *sPrefs = NULL;
// setjmp buffer for crash recovery. The crash handler longjmps here to
// return control to the shell's main loop after an app crashes.
static jmp_buf      sCrashJmp;
// Volatile because it's written from a signal handler context. Tells
// the recovery code which signal fired (for logging/diagnostics).
static volatile int sCrashSignal         = 0;
// Desktop update callback list (dynamic, managed via stb_ds arrput/arrdel)
typedef void (*DesktopUpdateFnT)(void);
static DesktopUpdateFnT *sDesktopUpdateFns = NULL;

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

static void idleYield(void *ctx);
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);

// ============================================================
// shellDesktopUpdate  -- notify desktop app of state change
// ============================================================

void shellDesktopUpdate(void) {
    for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
        sDesktopUpdateFns[i]();
    }
}


// ============================================================
// ctrlEscHandler  -- system-wide Ctrl+Esc callback
// ============================================================

static void ctrlEscHandler(void *ctx) {
    if (shellCtrlEscFn) {
        shellCtrlEscFn((AppContextT *)ctx);
    }
}


// ============================================================
// titleChangeHandler  -- refresh listeners when a window title changes
// ============================================================

static void titleChangeHandler(void *ctx) {
    (void)ctx;
    shellDesktopUpdate();
}


// ============================================================
// idleYield  -- called when no dirty rects need compositing
// ============================================================

// Registered as sCtx.idleCallback. dvxUpdate calls this when it has
// processed all pending events and there are no dirty rects to composite.
// Instead of busy-spinning, we yield to app tasks  -- this is where most
// of the CPU time for main-loop apps comes from when the UI is idle.
// The tsActiveCount > 1 check avoids the overhead of a tsYield call
// (which would do a scheduler scan) when the shell is the only task.
static void idleYield(void *ctx) {
    (void)ctx;

    if (tsActiveCount() > 1) {
        tsYield();
    }
}


static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) {
    (void)userData;
    dvxLog("  %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp);
}


// ============================================================


// ============================================================
// shellRegisterDesktopUpdate
// ============================================================

void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
    arrput(sDesktopUpdateFns, updateFn);
}


// ============================================================
// shellUnregisterDesktopUpdate
// ============================================================

void shellUnregisterDesktopUpdate(void (*updateFn)(void)) {
    for (int32_t i = 0; i < arrlen(sDesktopUpdateFns); i++) {
        if (sDesktopUpdateFns[i] == updateFn) {
            arrdel(sDesktopUpdateFns, i);
            return;
        }
    }
}


// ============================================================
// shellMain -- entry point called by the DVX loader
// ============================================================

int shellMain(int argc, char *argv[]) {
    (void)argc;
    (void)argv;

    dvxLog("DVX Shell starting...");

    // Load preferences (missing file or keys silently use defaults)
    sPrefs = prefsLoad("CONFIG/DVX.INI");

    // Initialize task system (no GUI needed)
    if (tsInit() != TS_OK) {
        dvxLog("Failed to initialize task system");
        dvxShutdown(&sCtx);
        return 1;
    }

    // Shell task (task 0) gets high priority so the UI remains responsive
    // even when app tasks are CPU-hungry. With HIGH priority (11 credits
    // per epoch) vs app tasks at NORMAL (6 credits), the shell gets
    // roughly twice as many scheduling turns as any single app.
    tsSetPriority(0, TS_PRIORITY_HIGH);

    // Gather system information (CPU, memory, drives, etc.)
    shellInfoInit(&sCtx);

    // Initialize app slot table
    shellAppInit();

    // Point the memory tracker at currentAppId so allocations are
    // attributed to whichever app is currently executing.
    dvxMemAppIdPtr = &sCtx.currentAppId;

    // Set up idle callback for cooperative yielding. When dvxUpdate has
    // no work to do (no input events, no dirty rects), it calls this
    // instead of busy-looping. This is the main mechanism for giving
    // app tasks CPU time during quiet periods.

    // Install crash handler before loading apps so faults during app
    // initialization are caught and recovered from gracefully.
    platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, dvxLog);

    // Initialize GUI — switch from VGA splash to VESA mode as late as possible
    // so the VGA loading splash stays visible through all the init above.
    {
        int32_t videoW   = prefsGetInt(sPrefs, "video", "width",  640);
        int32_t videoH   = prefsGetInt(sPrefs, "video", "height", 480);
        int32_t videoBpp = prefsGetInt(sPrefs, "video", "bpp",    16);
        dvxLog("Preferences: video %ldx%ld %ldbpp", (long)videoW, (long)videoH, (long)videoBpp);

        int32_t result = dvxInit(&sCtx, videoW, videoH, videoBpp);

        if (result != 0) {
            dvxLog("Failed to initialize DVX GUI (error %ld)", (long)result);
            tsShutdown();
            return 1;
        }

        // Set callbacks AFTER dvxInit (which memsets the context to zero)
        sCtx.idleCallback   = idleYield;
        sCtx.idleCtx        = &sCtx;
        sCtx.onCtrlEsc      = ctrlEscHandler;
        sCtx.ctrlEscCtx     = &sCtx;
        sCtx.onTitleChange  = titleChangeHandler;
        sCtx.titleChangeCtx = &sCtx;

        dvxLog("Available video modes:");
        platformVideoEnumModes(logVideoMode, NULL);
        dvxLog("Selected: %ldx%ld %ldbpp (pitch %ld)", (long)sCtx.display.width, (long)sCtx.display.height, (long)sCtx.display.format.bitsPerPixel, (long)sCtx.display.pitch);

        // Apply mouse preferences
        const char *wheelStr = prefsGetString(sPrefs, "mouse", "wheel", "normal");
        int32_t     wheelDir = (strcmp(wheelStr, "reversed") == 0) ? -1 : 1;
        int32_t     dblClick = prefsGetInt(sPrefs, "mouse", "doubleclick", 500);
        const char *accelStr = prefsGetString(sPrefs, "mouse", "acceleration", "medium");
        int32_t     accelVal = 0;

        if (strcmp(accelStr, "off") == 0)          { accelVal = 10000; }
        else if (strcmp(accelStr, "low") == 0)     { accelVal = 100; }
        else if (strcmp(accelStr, "medium") == 0)  { accelVal = 64; }
        else if (strcmp(accelStr, "high") == 0)    { accelVal = 32; }

        dvxSetMouseConfig(&sCtx, wheelDir, dblClick, accelVal);
        dvxLog("Preferences: mouse wheel=%s doubleclick=%ldms accel=%s", wheelStr, (long)dblClick, accelStr);

        // Apply saved color scheme
        bool colorsLoaded = false;

        for (int32_t i = 0; i < ColorCountE; i++) {
            const char *val = prefsGetString(sPrefs, "colors", dvxColorName((ColorIdE)i), NULL);

            if (val) {
                int r, g, b;

                if (sscanf(val, "%d,%d,%d", &r, &g, &b) == 3) {
                    sCtx.colorRgb[i][0] = (uint8_t)r;
                    sCtx.colorRgb[i][1] = (uint8_t)g;
                    sCtx.colorRgb[i][2] = (uint8_t)b;
                    colorsLoaded = true;
                }
            }
        }

        if (colorsLoaded) {
            dvxApplyColorScheme(&sCtx);
            dvxLog("Preferences: loaded custom color scheme");
        }

        // Apply saved wallpaper
        const char *wpMode = prefsGetString(sPrefs, "desktop", "mode", "stretch");

        if (strcmp(wpMode, "tile") == 0)           { sCtx.wallpaperMode = WallpaperTileE; }
        else if (strcmp(wpMode, "center") == 0)    { sCtx.wallpaperMode = WallpaperCenterE; }
        else                                       { sCtx.wallpaperMode = WallpaperStretchE; }

        const char *wpPath = prefsGetString(sPrefs, "desktop", "wallpaper", NULL);

        if (wpPath) {
            if (dvxSetWallpaper(&sCtx, wpPath)) {
                dvxLog("Preferences: loaded wallpaper %s (%s)", wpPath, wpMode);
            } else {
                dvxLog("Preferences: failed to load wallpaper %s", wpPath);
            }
        }
    }

    // Load the desktop app (configurable via [shell] desktop= in dvx.ini)
    const char *desktopApp = prefsGetString(sPrefs, "shell", "desktop", SHELL_DESKTOP_APP);
    int32_t desktopId = shellLoadApp(&sCtx, desktopApp);

    if (desktopId < 0) {
        dvxLog("Failed to load desktop app '%s'", desktopApp);
        tsShutdown();
        dvxShutdown(&sCtx);
        return 1;
    }


    dvxLog("DVX Shell ready.");

    // Set recovery point for crash handler. setjmp returns 0 on initial
    // call (falls through to the main loop). On a crash, longjmp makes
    // setjmp return non-zero, entering this recovery block. The recovery
    // code runs on the main task's stack (restored by longjmp) so it's
    // safe to call any shell function.
    if (setjmp(sCrashJmp) != 0) {
        // Returned here from crash handler via longjmp.
        // The task switcher's currentIdx still points to the crashed task.
        // Fix it before doing anything else so the scheduler is consistent.
        tsRecoverToMain();

        // Platform handler already logged signal name and register dump.
        // Log app-specific info here.
        dvxLog("Current app ID: %ld", (long)sCtx.currentAppId);

        if (sCtx.currentAppId > 0) {
            ShellAppT *crashedApp = shellGetApp(sCtx.currentAppId);

            if (crashedApp) {
                dvxLog("App name: %s", crashedApp->name);
                dvxLog("App path: %s", crashedApp->path);
                dvxLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no");
                dvxLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId);
            }
        } else {
            dvxLog("Crashed in shell (task 0)");
        }

        dvxLog("Recovering from crash, killing app %ld", (long)sCtx.currentAppId);

        // Clear busy cursor so the fault dialog is interactive
        dvxSetBusy(&sCtx, false);

        if (sCtx.currentAppId > 0) {
            ShellAppT *app = shellGetApp(sCtx.currentAppId);

            if (app) {
                char msg[256];
                snprintf(msg, sizeof(msg), "'%s' has caused a fault and will be terminated.", app->name);
                shellForceKillApp(&sCtx, app);
                sCtx.currentAppId = 0;
                dvxMessageBox(&sCtx, "Application Error", msg, MB_OK | MB_ICONERROR);
            }
        }

        sCtx.currentAppId = 0;
        sCrashSignal  = 0;
        shellDesktopUpdate();
    }

    // Main loop  -- runs until dvxQuit() sets sCtx.running = false.
    // Two yield points per iteration: one explicit (below) and one via
    // the idle callback inside dvxUpdate. The explicit yield here ensures
    // app tasks get CPU time even during busy frames (lots of repaints).
    // Without it, a flurry of mouse-move events could starve app tasks
    // because dvxUpdate would keep finding work to do and never call idle.
    while (sCtx.running) {
        dvxUpdate(&sCtx);

        // Give app tasks CPU time even during active frames
        if (tsActiveCount() > 1) {
            tsYield();
        }

        // Reap terminated apps and notify desktop if anything changed.
        // This is the safe point for cleanup  -- we're at the top of the
        // main loop, not inside any callback or compositor operation.
        if (shellReapApps(&sCtx)) {
            shellDesktopUpdate();
        }
    }

    dvxLog("DVX Shell shutting down...");

    // Clean shutdown: terminate all apps first (destroys windows, kills
    // tasks, closes DXE handles), then tear down the task system and GUI
    // in reverse initialization order.
    shellTerminateAllApps(&sCtx);

    tsShutdown();
    dvxShutdown(&sCtx);
    prefsClose(sPrefs);
    sPrefs = NULL;

    dvxLog("DVX Shell exited.");
    return 0;
}
