// 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.

// 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 "shellInf.h"
#include "dvxDlg.h"
#include "dvxDraw.h"
#include "dvxVideo.h"
#include "dvxComp.h"
#include "dvxPrefs.h"
#include "dvxPlat.h"
#include "stb_ds_wrap.h"

#include <inttypes.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).
// Each entry records the appId that registered it (captured from
// sCtx.currentAppId at registration time) so shellPurgeAppCallbacks can drop
// a force-killed app's callbacks without depending on the app's shutdownFn.
typedef struct {
    void  (*fn)(void);
    int32_t appId;
} DesktopUpdateHandlerT;

static DesktopUpdateHandlerT *sDesktopUpdateFns = NULL;

// Idle handler list (dynamic, managed via stb_ds arrput/arrdel).  Walked
// every idle frame by shellIdleDispatch.  Serial/secLink pollers register
// here instead of overwriting the context's single idleCallback slot, so a
// background connection keeps pumping regardless of which app is foreground
// and the shell's cooperative yield can never be clobbered.  appId is the
// owner captured at registration so shellPurgeAppCallbacks can reclaim it.
typedef struct {
    void  (*fn)(void *ctx);
    void   *ctx;
    int32_t appId;
} IdleHandlerT;

static IdleHandlerT *sIdleHandlers = NULL;

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

static void appIdRestore(int32_t value);
static int32_t appIdSave(void);
static void ctrlEscHandler(void *ctx);
static void f1HelpHandler(void *ctx);
static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData);
void        shellDesktopUpdate(void);
static void shellIdleDispatch(void *ctx);
int         shellMain(int argc, char *argv[]);
void        shellPurgeAppCallbacks(int32_t appId);
void        shellRegisterDesktopUpdate(void (*updateFn)(void));
void        shellRegisterIdle(void (*fn)(void *ctx), void *ctx);
void        shellUnregisterDesktopUpdate(void (*updateFn)(void));
void        shellUnregisterIdle(void (*fn)(void *ctx), void *ctx);
static void titleChangeHandler(void *ctx);


// Task-switch hooks: currentAppId is semantically per-task (each app
// task runs as its own appId; the shell's main task is 0).  Without
// save/restore on every switch, the first yield back from an app left
// the shell's allocations and windows attributed to that app.
static void appIdRestore(int32_t value) {
    sCtx.currentAppId = value;
}


static int32_t appIdSave(void) {
    return sCtx.currentAppId;
}


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


static void f1HelpHandler(void *ctx) {
    AppContextT *ac = (AppContextT *)ctx;

    // Find the focused window's owning app
    char args[SHELL_ARGS_MAX] = {0};
    int32_t focusedAppId = 0;

    if (ac->stack.focusedIdx >= 0) {
        WindowT *win = ac->stack.windows[ac->stack.focusedIdx];
        focusedAppId = win->appId;
    }

    if (focusedAppId > 0) {
        ShellAppT *app = shellGetApp(focusedAppId);

        if (app && app->dxeCtx) {
            // Let the app refresh helpTopic based on current context
            if (app->dxeCtx->onHelpQuery) {
                app->dxeCtx->onHelpQuery(app->dxeCtx->helpQueryCtx);
            }

            if (app->dxeCtx->helpFile[0]) {
                if (app->dxeCtx->helpTopic[0]) {
                    snprintf(args, sizeof(args), "%s %s",
                             app->dxeCtx->helpFile, app->dxeCtx->helpTopic);
                } else {
                    snprintf(args, sizeof(args), "%s", app->dxeCtx->helpFile);
                }
            }
        }
    }

    // Fall back to system help if no app-specific help
    if (!args[0]) {
        snprintf(args, sizeof(args), "APPS" DVX_PATH_SEP "KPUNCH" DVX_PATH_SEP "PROGMAN" DVX_PATH_SEP "DVXHELP.HLP");
    }

    shellLoadAppWithArgs(ac, DVX_HELP_VIEWER_APP_LITERAL, args);
}


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);
}


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


// Installed as the context's single idleCallback.  dvxUpdate calls this when
// all events are processed and nothing is dirty.  We walk the idle handler
// list (serial/secLink pollers register here), then perform the cooperative
// yield unconditionally as the tail step -- so a registered poller can never
// suppress task switching the way the old single-slot scheme did, where BASIC
// overwrote idleYield outright and never restored it.  The tsActiveCount > 1
// check avoids a scheduler scan when the shell is the only task.  arrlen is
// re-read each iteration so a handler that unregisters one mid-walk (e.g. via
// a nested modal dvxUpdate) cannot index past the array.
static void shellIdleDispatch(void *ctx) {
    (void)ctx;

    for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
        if (sIdleHandlers[i].fn) {
            sIdleHandlers[i].fn(sIdleHandlers[i].ctx);
        }
    }

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


// Drop every idle and desktop-update callback registered by the given app,
// matched on the appId captured when each was registered.  shellForceKillApp
// (and, defensively, shellReapApp) calls this before dlclose so a force or
// crash kill reclaims an app's shell-side registrations WITHOUT running the
// app's own -- possibly hung or faulting -- shutdownFn.  Otherwise the
// pointers would dangle into unmapped DXE code and the next shellDesktopUpdate
// or idle frame would jump into it.  appId < 1 (the shell's own and
// persistent-DXE registrations) is never purged.  Both lists are walked
// back-to-front so arrdel's index shifts cannot skip a matching entry.
void shellPurgeAppCallbacks(int32_t appId) {
    if (appId < 1) {
        return;
    }

    for (int32_t i = arrlen(sDesktopUpdateFns) - 1; i >= 0; i--) {
        if (sDesktopUpdateFns[i].appId == appId) {
            arrdel(sDesktopUpdateFns, i);
        }
    }

    for (int32_t i = arrlen(sIdleHandlers) - 1; i >= 0; i--) {
        if (sIdleHandlers[i].appId == appId) {
            arrdel(sIdleHandlers, i);
        }
    }
}


void shellRegisterDesktopUpdate(void (*updateFn)(void)) {
    DesktopUpdateHandlerT handler;

    handler.fn    = updateFn;
    handler.appId = sCtx.currentAppId;
    arrput(sDesktopUpdateFns, handler);
}


// Idempotent: a (fn, ctx) pair already present is not added twice, so a
// producer can call this on every attach without tracking its own state.
void shellRegisterIdle(void (*fn)(void *ctx), void *ctx) {
    IdleHandlerT handler;

    for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
        if (sIdleHandlers[i].fn == fn && sIdleHandlers[i].ctx == ctx) {
            return;
        }
    }

    handler.fn    = fn;
    handler.ctx   = ctx;
    handler.appId = sCtx.currentAppId;
    arrput(sIdleHandlers, handler);
}


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


// No-op if the (fn, ctx) pair is absent, so double-unregister is safe.
void shellUnregisterIdle(void (*fn)(void *ctx), void *ctx) {
    for (int32_t i = 0; i < arrlen(sIdleHandlers); i++) {
        if (sIdleHandlers[i].fn == fn && sIdleHandlers[i].ctx == ctx) {
            arrdel(sIdleHandlers, i);
            return;
        }
    }
}


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


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(DVX_INI_PATH);

    // Initialize task system (no GUI needed)
    if (tsInit() != TS_OK) {
        dvxLog("Failed to initialize task system");
        // dvxInit has not run yet at this point, so there is nothing to shut down.
        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);

    // Initialize app slot table
    shellAppInit();

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

    // Make currentAppId task-local: saved/restored on every task switch
    // so the shell's baseline (0) is not clobbered by app main loops.
    tsSetContextHooks(appIdSave, appIdRestore);

    // 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",  DVX_DEFAULT_VIDEO_W);
        int32_t videoH   = prefsGetInt(sPrefs, "video", "height", DVX_DEFAULT_VIDEO_H);
        int32_t videoBpp = prefsGetInt(sPrefs, "video", "bpp",    DVX_DEFAULT_VIDEO_BPP);
        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   = shellIdleDispatch;
        sCtx.idleCtx        = &sCtx;
        sCtx.onCtrlEsc      = ctrlEscHandler;
        sCtx.ctrlEscCtx     = &sCtx;
        sCtx.onF1           = f1HelpHandler;
        sCtx.f1Ctx          = &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);

        // Gather system information (CPU, memory, video, drives, etc.)
        // after dvxInit so the display fields have real values to report.
        shellInfoInit(&sCtx);

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

        if (strcmp(accelStr, "off") == 0) {
            accelVal = MOUSE_ACCEL_OFF;
        } else if (strcmp(accelStr, "low") == 0) {
            accelVal = MOUSE_ACCEL_LOW;
        } else if (strcmp(accelStr, "medium") == 0) {
            accelVal = MOUSE_ACCEL_MEDIUM;
        } else if (strcmp(accelStr, "high") == 0) {
            accelVal = MOUSE_ACCEL_HIGH;
        }

        int32_t speed     = prefsGetInt(sPrefs, "mouse", "speed", MOUSE_SPEED_DEFAULT);
        int32_t wheelStep = prefsGetInt(sPrefs, "mouse", "wheelspeed", MOUSE_WHEEL_STEP_DEFAULT);

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

        // 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) {
                int32_t r;
                int32_t g;
                int32_t b;

                if (sscanf(val, "%" SCNd32 ",%" SCNd32 ",%" SCNd32, &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.
        // Capture the crashed task's app id BEFORE tsRecoverToMain, which
        // restores the main task's context value (currentAppId back to 0).
        int32_t crashedAppId = sCtx.currentAppId;

        // The task switcher's currentIdx still points to the crashed task.
        // Fix it before doing anything else so the scheduler is consistent.
        tsRecoverToMain();

        // The longjmp discarded every in-flight dvxUpdate/dispatch frame
        // before its dispatchDepth-- could run, so reset the counter here.
        // Otherwise dvxDestroyWindow would defer forever (including the
        // crashed app's own windows below, past its DXE being closed) and
        // flushPendingDestroys would never fire again.
        sCtx.dispatchDepth = 0;

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

        if (crashedAppId > 0) {
            ShellAppT *crashedApp = shellGetApp(crashedAppId);

            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)crashedAppId);

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

        if (crashedAppId > 0) {
            ShellAppT *app = shellGetApp(crashedAppId);

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

        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;
}
