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

// loaderMain.c -- DVX bootstrap loader entry point
//
// Loads all DXE modules from two directories:
//   libs/     *.lib  -- core libraries (libtasks, libdvx, dvxshell)
//   widgets/  *.wgt  -- widget type plugins (box, button, listview, etc.)
//
// Each module may have a .dep file (same base name, .dep extension)
// listing base names of modules that must be loaded before it.
// The loader resolves the dependency graph and loads in topological
// order.  After loading, any module that exports wgtRegister() has
// it called.  Finally, the loader finds and calls shellMain().

#include "stddclmr.h"
#include "dvxPlat.h"
#include "dvxPrefs.h"
#include "../tools/hlpcCompile.h"

#include <ctype.h>
#include <dlfcn.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>

// The loader is not a DXE — use plain realloc/free for stb_ds so that
// all translation units (loaderMain.o, dvxPrefs.o) share the same heap.
#define STB_DS_IMPLEMENTATION
#include "stb_ds_wrap.h"

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

#define LIBS_DIR      "LIBS"
#define WIDGET_DIR    "WIDGETS"
#define LOG_PATH      "dvx.log"

// ============================================================
// Splash screen (delegates to platformSplash* in dvxPlatformDos.c)
// ============================================================

// Palette indices for progress bar (indices into SPLASH.RAW palette)
#define SPLASH_BAR_BG   50   // dark gray (RGB 68,68,68)
#define SPLASH_BAR_FG   135  // light gray (RGB 168,168,168)
#define SPLASH_BAR_OUT  45   // darker gray (RGB 60,60,60)

// Progress bar geometry
#define PBAR_X      10
#define PBAR_Y      188
#define PBAR_W      300
#define PBAR_H      6

// ============================================================
// Module entry for dependency resolution
// ============================================================

typedef struct {
    char   path[DVX_MAX_PATH];
    char   baseName[16];
    char **deps;
    bool   loaded;
    void  *handle;
} ModuleT;

// Scratch state for processHcf callback — collects output=, imagedir=, and
// the accumulated source= glob expansions in one pass.
typedef struct {
    const char  *hcfDir;
    char         outputFile[DVX_MAX_PATH];
    char         imgDir[DVX_MAX_PATH];
    char       **inputFiles;
} ProcessHcfCtxT;

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

static int32_t sSplashActive    = 0;
static int32_t sSplashTotal     = 0;
static int32_t sSplashLoaded    = 0;

static int32_t sLibCount    = 0;
static int32_t sWidgetCount = 0;

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

static bool     allDepsLoaded(const ModuleT *mod, const ModuleT *mods);
static void     collectGlobFiles(char ***outFiles, const char *pattern, const char *excludePattern);
static void     countHcfCallback(const char *key, const char *val, const char *exclude, void *user);
static int32_t  countHcfInputFiles(const char *hcfPath);
static int32_t  countTotalHelpSteps(const char *dirPath);
void            dvxLog(const char *fmt, ...);
static void     extractBaseName(const char *path, const char *ext, char *out, int32_t outSize);
static void    *findSymbol(void **handles, const char *symbol);
static void     freeMods(ModuleT *mods);
static void     hcfForEachEntry(const char *hcfPath, void (*cb)(const char *key, const char *val, const char *exclude, void *user), void *user);
static void     helpRecompileIfNeeded(void);
static void     hlpcProgressCallback(void *ctx, int32_t current, int32_t total);
static void   **loadAllModules(void);
static void     loadInOrder(ModuleT *mods);
static void     logAndReadDeps(ModuleT *mods);
bool            platformGlobMatch(const char *pattern, const char *name);
static void     processHcf(const char *hcfPath, const char *hcfDir);
static void     processHcfCallback(const char *key, const char *val, const char *exclude, void *user);
static void     processHcfDir(const char *dirPath);
static void     readDeps(ModuleT *mod);
static void     scanDir(const char *dirPath, const char *ext, ModuleT **mods);
static void     splashDrawScreen(void);
static void     splashShutdownIfActive(void);
static void     splashUpdateProgress(void);
static void     validateDeps(const ModuleT *libs, const ModuleT *widgets);
int             main(int argc, char *argv[]);


// A dep is satisfied if either:
//   1. No module with that base name exists (external, assumed OK)
//   2. The module with that base name is already loaded
static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods) {
    for (int32_t d = 0; d < arrlen(mod->deps); d++) {
        for (int32_t j = 0; j < arrlen(mods); j++) {
            if (strcasecmp(mods[j].baseName, mod->deps[d]) == 0) {
                if (!mods[j].loaded) {
                    return false;
                }

                break;
            }
        }
    }

    return true;
}


// Collect matching filenames into an stb_ds array of strdup'd paths.
static void collectGlobFiles(char ***outFiles, const char *pattern, const char *excludePattern) {
    char dirPart[DVX_MAX_PATH];
    const char *globPart = NULL;

    snprintf(dirPart, sizeof(dirPart), "%s", pattern);
    char *lastSep = platformPathDirEnd(dirPart);

    if (lastSep) {
        *lastSep = '\0';
        globPart = lastSep + 1;
    } else {
        globPart = pattern;
        dirPart[0] = '.';
        dirPart[1] = '\0';
    }

    char **names = dvxReadDir(dirPart);

    if (!names) {
        return;
    }

    int32_t nEntries = (int32_t)arrlen(names);

    for (int32_t i = 0; i < nEntries; i++) {
        // Skip hidden files (dvxReadDir already strips "." and "..")
        if (names[i][0] == '.') {
            continue;
        }

        char fullPath[DVX_MAX_PATH];
        snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPart, names[i]);

        struct stat st;

        if (stat(fullPath, &st) != 0) {
            continue;
        }

        if (S_ISDIR(st.st_mode)) {
            char subPattern[DVX_MAX_PATH];
            snprintf(subPattern, sizeof(subPattern), "%s" DVX_PATH_SEP "%s", fullPath, globPart);
            collectGlobFiles(outFiles, subPattern, excludePattern);
        } else if (platformGlobMatch(globPart, names[i])) {
            if (!excludePattern || !platformGlobMatch(excludePattern, names[i])) {
                arrput(*outFiles, strdup(fullPath));
            }
        }
    }

    dvxReadDirFree(names);
}


// Callback for countHcfInputFiles: expand source= globs into the user file list.
static void countHcfCallback(const char *key, const char *val, const char *exclude, void *user) {
    if (strcasecmp(key, "source") == 0) {
        collectGlobFiles((char ***)user, val, exclude);
    }
}


// Count input files for a single .hcf config (for progress total calculation).
static int32_t countHcfInputFiles(const char *hcfPath) {
    char **files = NULL;
    hcfForEachEntry(hcfPath, countHcfCallback, &files);

    int32_t count = (int32_t)arrlen(files);

    for (int32_t i = 0; i < count; i++) {
        free(files[i]);
    }

    arrfree(files);
    return count;
}


// Count total progress steps across all .hcf files under a directory.
static int32_t countTotalHelpSteps(const char *dirPath) {
    char **names = dvxReadDir(dirPath);

    if (!names) {
        return 0;
    }

    int32_t total    = 0;
    int32_t nEntries = (int32_t)arrlen(names);

    for (int32_t i = 0; i < nEntries; i++) {
        if (names[i][0] == '.') {
            continue;
        }

        char fullPath[DVX_MAX_PATH];
        snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);

        struct stat st;

        if (stat(fullPath, &st) != 0) {
            continue;
        }

        if (S_ISDIR(st.st_mode)) {
            total += countTotalHelpSteps(fullPath);
        } else if (dvxHasExt(names[i], ".hcf")) {
            int32_t fileCount = countHcfInputFiles(fullPath);
            total += hlpcProgressTotal(fileCount);
        }
    }

    dvxReadDirFree(names);
    return total;
}


// Global logging function exported to all DXE modules.
// Opens/closes the file per-write so it's never held open.
void dvxLog(const char *fmt, ...) {
    FILE *f = fopen(LOG_PATH, "a");

    if (!f) {
        return;
    }

    va_list ap;
    va_start(ap, fmt);
    vfprintf(f, fmt, ap);
    va_end(ap);
    fprintf(f, "\n");
    fclose(f);
}


static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize) {
    // Find last directory separator
    const char *start = path;
    const char *p     = path;

    while (*p) {
        if (*p == '/' || *p == '\\') {
            start = p + 1;
        }

        p++;
    }

    // Copy up to the extension
    int32_t extLen = strlen(ext);
    int32_t len    = strlen(start);

    if (len > extLen && strcasecmp(start + len - extLen, ext) == 0) {
        len -= extLen;
    }

    if (len >= outSize) {
        len = outSize - 1;
    }

    for (int32_t i = 0; i < len; i++) {
        out[i] = tolower((unsigned char)start[i]);
    }

    out[len] = '\0';
}


static void *findSymbol(void **handles, const char *symbol) {
    for (int32_t i = 0; i < arrlen(handles); i++) {
        void *sym = dlsym(handles[i], symbol);

        if (sym) {
            return sym;
        }
    }

    return NULL;
}


static void freeMods(ModuleT *mods) {
    for (int32_t i = 0; i < arrlen(mods); i++) {
        for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
            free(mods[i].deps[d]);
        }

        arrfree(mods[i].deps);
    }

    arrfree(mods);
}


// Parse a .hcf INI-like file and invoke cb for each key=value entry.
// Lines starting with # and blank lines are skipped.  For source= entries,
// the value may include a "!excludePattern" suffix which is split out and
// passed separately.  For other keys, exclude is "".
static void hcfForEachEntry(const char *hcfPath, void (*cb)(const char *key, const char *val, const char *exclude, void *user), void *user) {
    FILE *f = fopen(hcfPath, "r");

    if (!f) {
        return;
    }

    char line[512];

    while (fgets(line, (int)sizeof(line), f)) {
        dvxTrimRight(line);

        if (line[0] == '#' || line[0] == '\0') {
            continue;
        }

        // Split "key = value" - key is the run of chars up to space or '='
        const char *p   = line;
        int32_t     klen = 0;

        while (p[klen] && p[klen] != ' ' && p[klen] != '=') {
            klen++;
        }

        char key[32] = {0};

        if (klen >= (int32_t)sizeof(key)) {
            klen = (int32_t)sizeof(key) - 1;
        }

        memcpy(key, p, klen);
        key[klen] = '\0';

        p += klen;

        while (*p == ' ' || *p == '=') { p++; }

        // For source=, split out !exclude suffix
        if (strcasecmp(key, "source") == 0) {
            char        pattern[DVX_MAX_PATH] = {0};
            char        exclude[DVX_MAX_PATH] = {0};
            const char *bang = strchr(p, '!');

            if (bang) {
                int32_t patLen = (int32_t)(bang - p);

                while (patLen > 0 && p[patLen - 1] == ' ') { patLen--; }

                memcpy(pattern, p, patLen);
                pattern[patLen] = '\0';
                bang++;

                while (*bang == ' ') { bang++; }

                snprintf(exclude, sizeof(exclude), "%s", bang);
            } else {
                snprintf(pattern, sizeof(pattern), "%s", p);
            }

            cb(key, pattern, exclude[0] ? exclude : NULL, user);
        } else {
            cb(key, p, NULL, user);
        }
    }

    fclose(f);
}


static void helpRecompileIfNeeded(void) {
    PrefsHandleT *ini = prefsLoad(DVX_INI_PATH);

    if (!ini) {
        dvxLog("helpRecompile: cannot load INI");
        return;
    }

    int32_t storedLibs    = prefsGetInt(ini, "help", "libCount", -1);
    int32_t storedWidgets = prefsGetInt(ini, "help", "widgetCount", -1);

    // First startup: no [help] section. Save counts, skip recompile.
    if (storedLibs == -1 && storedWidgets == -1) {
        dvxLog("helpRecompile: first startup, saving initial counts (libs=%d widgets=%d)",
               (int)sLibCount, (int)sWidgetCount);
        prefsSetInt(ini, "help", "libCount", sLibCount);
        prefsSetInt(ini, "help", "widgetCount", sWidgetCount);
        prefsSave(ini);
        prefsClose(ini);
        return;
    }

    // Counts unchanged: no recompile needed
    if (storedLibs == sLibCount && storedWidgets == sWidgetCount) {
        prefsClose(ini);
        return;
    }

    dvxLog("helpRecompile: counts changed (libs: %d->%d, widgets: %d->%d)",
           (int)storedLibs, (int)sLibCount, (int)storedWidgets, (int)sWidgetCount);

    // Count total progress steps across all .hcf compilations
    int32_t totalSteps = countTotalHelpSteps("APPS");

    if (totalSteps == 0) {
        dvxLog("helpRecompile: no help sources found");
        prefsSetInt(ini, "help", "libCount", sLibCount);
        prefsSetInt(ini, "help", "widgetCount", sWidgetCount);
        prefsSave(ini);
        prefsClose(ini);
        return;
    }

    // Reset progress bar for recompile phase
    sSplashLoaded = 0;
    sSplashTotal  = totalSteps;
    platformSplashFillRect(PBAR_X, PBAR_Y, PBAR_W, PBAR_H, SPLASH_BAR_BG);

    // Recursively scan APPS/ for .hcf files and process each
    processHcfDir("APPS");

    // Save new counts
    prefsSetInt(ini, "help", "libCount", sLibCount);
    prefsSetInt(ini, "help", "widgetCount", sWidgetCount);
    prefsSave(ini);
    prefsClose(ini);

    dvxLog("helpRecompile: done");
}


// Progress callback for hlpcCompile — updates the splash progress bar.
static void hlpcProgressCallback(void *ctx, int32_t current, int32_t total) {
    (void)ctx;
    (void)total;
    (void)current;
    sSplashLoaded++;
    splashUpdateProgress();
}


static void **loadAllModules(void) {
    void **handles = NULL;

    // Scan both directories first to get total module count for progress bar
    ModuleT *libs = NULL;
    scanDir(LIBS_DIR, ".lib", &libs);
    logAndReadDeps(libs);

    ModuleT *widgets = NULL;
    scanDir(WIDGET_DIR, ".wgt", &widgets);
    logAndReadDeps(widgets);

    sLibCount    = (int32_t)arrlen(libs);
    sWidgetCount = (int32_t)arrlen(widgets);
    sSplashTotal = sLibCount + sWidgetCount;

    // Abort early on typos or missing prerequisite .lib/.wgt files.
    validateDeps(libs, widgets);

    // Phase 1: load libraries in dependency order
    loadInOrder(libs);

    for (int32_t i = 0; i < arrlen(libs); i++) {
        if (libs[i].handle) {
            arrput(handles, libs[i].handle);
        }
    }

    freeMods(libs);

    // Phase 2: load widgets in dependency order (all libs already loaded)
    loadInOrder(widgets);

    for (int32_t i = 0; i < arrlen(widgets); i++) {
        if (widgets[i].handle) {
            arrput(handles, widgets[i].handle);
        }
    }

    freeMods(widgets);

    return handles;
}


// Repeatedly scans the module list for entries whose dependencies
// are all satisfied, loads them, and marks them done.  Stops when
// all modules are loaded or no progress can be made (circular dep).
static void loadInOrder(ModuleT *mods) {
    typedef void (*RegFnT)(void);
    int32_t total  = arrlen(mods);
    int32_t loaded = 0;
    bool    progress;

    do {
        progress = false;

        for (int32_t i = 0; i < total; i++) {
            if (mods[i].loaded) {
                continue;
            }

            if (!allDepsLoaded(&mods[i], mods)) {
                continue;
            }

            dvxLog("Loading: %s", mods[i].path);
            mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL);

            if (!mods[i].handle) {
                const char *err = dlerror();
                dvxLog("  FAILED: %s", err ? err : "(unknown)");
                splashShutdownIfActive();
                fprintf(stderr, "FATAL: Failed to load %s\n  %s\n", mods[i].path, err ? err : "(unknown error)");
                exit(1);
            }

            RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister");

            if (regFn) {
                regFn();

                // Record the .wgt path for any newly registered ifaces
                typedef int32_t (*IfaceCountFnT)(void);
                typedef const void *(*IfaceAtFnT)(int32_t, const char **);
                typedef const char *(*IfaceGetPathFnT)(const char *);
                typedef void (*IfaceSetPathFnT)(const char *, const char *);

                IfaceCountFnT   countFn   = (IfaceCountFnT)dlsym(NULL, "_wgtIfaceCount");
                IfaceAtFnT      atFn      = (IfaceAtFnT)dlsym(NULL, "_wgtIfaceAt");
                IfaceGetPathFnT getPathFn = (IfaceGetPathFnT)dlsym(NULL, "_wgtIfaceGetPath");
                IfaceSetPathFnT setPathFn = (IfaceSetPathFnT)dlsym(NULL, "_wgtIfaceSetPath");

                if (countFn && atFn && getPathFn && setPathFn) {
                    int32_t ic = countFn();

                    for (int32_t k = 0; k < ic; k++) {
                        const char *ifaceName = NULL;
                        atFn(k, &ifaceName);

                        if (ifaceName && !getPathFn(ifaceName)) {
                            setPathFn(ifaceName, mods[i].path);
                        }
                    }
                }
            } else if (strstr(mods[i].path, ".wgt") || strstr(mods[i].path, ".WGT")) {
                dvxLog("  No _wgtRegister in %s", mods[i].baseName);
            }

            mods[i].loaded = true;
            loaded++;
            progress = true;
            sSplashLoaded++;
            splashUpdateProgress();
        }
    } while (progress && loaded < total);

    if (loaded < total) {
        fprintf(stderr, "Module loader: %d of %d modules could not be loaded (circular deps or missing deps)\n", (int)(total - loaded), (int)total);

        for (int32_t i = 0; i < total; i++) {
            if (!mods[i].loaded) {
                fprintf(stderr, "  %s\n", mods[i].path);
            }
        }
    }
}


static void logAndReadDeps(ModuleT *mods) {
    dvxLog("Discovered %d modules:", arrlen(mods));

    for (int32_t i = 0; i < arrlen(mods); i++) {
        dvxLog("  [%d] %s (base: %s)", i, mods[i].path, mods[i].baseName);
    }

    for (int32_t i = 0; i < arrlen(mods); i++) {
        readDeps(&mods[i]);

        if (arrlen(mods[i].deps) > 0) {
            char line[512];
            int32_t pos = snprintf(line, sizeof(line), "  %s deps:", mods[i].baseName);

            for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
                // dvxStrAppendf bounds the accumulate and returns -1 on
                // truncation (or a prior failure), so a long dep list can
                // never overrun the fixed line buffer.
                pos = dvxStrAppendf(line, sizeof(line), pos, " %s", mods[i].deps[d]);

                if (pos < 0) {
                    break;
                }
            }

            dvxLog("%s", line);
        }
    }
}


// Simple glob pattern matching for filenames. Supports:
//   *       matches zero or more characters
//   ?       matches exactly one character
//   !pat    at the start means "exclude files matching pat"
// Case-insensitive. Exported for use by DXE modules.
bool platformGlobMatch(const char *pattern, const char *name) {
    while (*pattern && *name) {
        if (*pattern == '*') {
            pattern++;

            if (!*pattern) {
                return true;
            }

            while (*name) {
                if (platformGlobMatch(pattern, name)) {
                    return true;
                }

                name++;
            }

            return false;
        } else if (*pattern == '?' || tolower((unsigned char)*pattern) == tolower((unsigned char)*name)) {
            pattern++;
            name++;
        } else {
            return false;
        }
    }

    // Skip trailing *
    while (*pattern == '*') {
        pattern++;
    }

    return *pattern == '\0' && *name == '\0';
}


// Callback for processHcf: accumulate output=, imagedir=, and expanded source= entries.
static void processHcfCallback(const char *key, const char *val, const char *exclude, void *user) {
    ProcessHcfCtxT *ctx = (ProcessHcfCtxT *)user;

    if (strcasecmp(key, "output") == 0) {
        snprintf(ctx->outputFile, sizeof(ctx->outputFile), "%s" DVX_PATH_SEP "%s", ctx->hcfDir, val);
    } else if (strcasecmp(key, "imagedir") == 0) {
        snprintf(ctx->imgDir, sizeof(ctx->imgDir), "%s", val);
    } else if (strcasecmp(key, "source") == 0) {
        collectGlobFiles(&ctx->inputFiles, val, exclude);
    }
}


static void processHcf(const char *hcfPath, const char *hcfDir) {
    ProcessHcfCtxT ctx;
    memset(&ctx, 0, sizeof(ctx));
    ctx.hcfDir = hcfDir;

    hcfForEachEntry(hcfPath, processHcfCallback, &ctx);

    int32_t inputCount = (int32_t)arrlen(ctx.inputFiles);

    if (!ctx.outputFile[0] || inputCount == 0) {
        for (int32_t i = 0; i < inputCount; i++) { free(ctx.inputFiles[i]); }
        arrfree(ctx.inputFiles);
        return;
    }

    dvxLog("helpRecompile: %s -> %s (%d files)", hcfPath, ctx.outputFile, (int)inputCount);

    int32_t rc = hlpcCompile((const char **)ctx.inputFiles, inputCount, ctx.outputFile,
                             ctx.imgDir[0] ? ctx.imgDir : NULL, NULL, HLPC_QUIET,
                             hlpcProgressCallback, NULL);

    if (rc != 0) {
        dvxLog("helpRecompile: FAILED (rc=%d)", (int)rc);
    }

    for (int32_t i = 0; i < inputCount; i++) {
        free(ctx.inputFiles[i]);
    }

    arrfree(ctx.inputFiles);
}


// Recursively scan a directory for .hcf files and process each one.
static void processHcfDir(const char *dirPath) {
    char **names = dvxReadDir(dirPath);

    if (!names) {
        return;
    }

    int32_t nEntries = (int32_t)arrlen(names);

    for (int32_t i = 0; i < nEntries; i++) {
        if (names[i][0] == '.') {
            continue;
        }

        char fullPath[DVX_MAX_PATH];
        snprintf(fullPath, sizeof(fullPath), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);

        struct stat st;

        if (stat(fullPath, &st) != 0) {
            continue;
        }

        if (S_ISDIR(st.st_mode)) {
            processHcfDir(fullPath);
        } else if (dvxHasExt(names[i], ".hcf")) {
            processHcf(fullPath, dirPath);
        }
    }

    dvxReadDirFree(names);
}


// The .dep file has the same path as the module but with a .dep
// extension.  Each line is a dependency base name.  Empty lines
// and lines starting with # are ignored.
static void readDeps(ModuleT *mod) {
    // Build dep file path: replace extension with .dep
    char depPath[DVX_MAX_PATH];
    strncpy(depPath, mod->path, sizeof(depPath) - 1);
    depPath[sizeof(depPath) - 1] = '\0';

    char *dot = strrchr(depPath, '.');

    if (!dot) {
        return;
    }

    strcpy(dot, ".dep");

    FILE *f = fopen(depPath, "r");

    if (!f) {
        return;
    }

    char line[64];

    while (fgets(line, sizeof(line), f)) {
        // A physical line longer than the buffer is split across fgets
        // calls.  Drain the remainder so an over-long comment's tail (which
        // does not start with '#') is not mistaken for a separate dep name.
        // fgetc returns EOF immediately at end of file, so this is safe even
        // for an unterminated final line.
        if (strchr(line, '\n') == NULL) {
            int32_t ch;

            while ((ch = fgetc(f)) != '\n' && ch != EOF) {
                // Discard the rest of the over-long physical line.
            }
        }

        // Strip trailing whitespace (CR/LF/space/tab)
        int32_t len = dvxTrimRight(line);

        // Skip empty lines and comments
        if (len == 0 || line[0] == '#') {
            continue;
        }

        // Lowercase the dep name for case-insensitive matching
        for (int32_t i = 0; i < len; i++) {
            line[i] = tolower((unsigned char)line[i]);
        }

        arrput(mod->deps, strdup(line));
    }

    fclose(f);
}


static void scanDir(const char *dirPath, const char *ext, ModuleT **mods) {
    // Collect all entries first, close the handle, then process.
    // DOS has limited file handles; keeping a DIR open during
    // recursion or stat() causes intermittent failures.
    char **names = dvxReadDir(dirPath);

    if (!names) {
        return;
    }

    int32_t count = (int32_t)arrlen(names);

    for (int32_t i = 0; i < count; i++) {
        char path[DVX_MAX_PATH];
        snprintf(path, sizeof(path), "%s" DVX_PATH_SEP "%s", dirPath, names[i]);

        if (dvxHasExt(names[i], ext)) {
            ModuleT mod;
            memset(&mod, 0, sizeof(mod));
            snprintf(mod.path, sizeof(mod.path), "%s", path);
            extractBaseName(path, ext, mod.baseName, sizeof(mod.baseName));
            arrput(*mods, mod);
        } else {
            struct stat st;

            if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
                scanDir(path, ext, mods);
            }
        }
    }

    dvxReadDirFree(names);
}


static void splashDrawScreen(void) {
    if (platformSplashLoadRaw("SYSTEM/SPLASH.RAW")) {
        platformSplashFillRect(PBAR_X - 1, PBAR_Y - 1, PBAR_W + 2, PBAR_H + 2, SPLASH_BAR_OUT);
        platformSplashFillRect(PBAR_X, PBAR_Y, PBAR_W, PBAR_H, SPLASH_BAR_BG);
    }
}


static void splashShutdownIfActive(void) {
    if (sSplashActive) {
        platformSplashShutdown();
        sSplashActive = 0;
    }
}


static void splashUpdateProgress(void) {
    if (!sSplashActive || sSplashTotal <= 0) {
        return;
    }

    int32_t fillW = (PBAR_W * sSplashLoaded) / sSplashTotal;

    if (fillW > PBAR_W) {
        fillW = PBAR_W;
    }

    platformSplashFillRect(PBAR_X, PBAR_Y, fillW, PBAR_H, SPLASH_BAR_FG);
}


// allDepsLoaded silently treats an unknown dep name as "external, assumed
// OK" so that libc / platform symbols don't trip the topological sort.
// That's fine for intentional externals but masks typos and missing
// .lib/.wgt files.  This pre-flight check ensures every name in any .dep
// resolves to a module we actually discovered; anything else is fatal.
static void validateDeps(const ModuleT *libs, const ModuleT *widgets) {
    const ModuleT *pools[2] = { libs, widgets };
    const char    *kinds[2] = { "lib", "widget" };
    int32_t        errors   = 0;

    for (int32_t pi = 0; pi < 2; pi++) {
        const ModuleT *mods = pools[pi];

        for (int32_t i = 0; i < arrlen(mods); i++) {
            for (int32_t d = 0; d < arrlen(mods[i].deps); d++) {
                const char *depName = mods[i].deps[d];
                bool        found   = false;

                for (int32_t qi = 0; qi < 2 && !found; qi++) {
                    const ModuleT *pool = pools[qi];

                    for (int32_t j = 0; j < arrlen(pool); j++) {
                        if (strcasecmp(pool[j].baseName, depName) == 0) {
                            found = true;
                            break;
                        }
                    }
                }

                if (!found) {
                    dvxLog("FATAL: %s %s requires missing dep: %s", kinds[pi], mods[i].baseName, depName);

                    // Restore text mode before the first print or the
                    // message prints invisibly under the graphics-mode
                    // splash.
                    splashShutdownIfActive();

                    fprintf(stderr, "FATAL: %s %s requires missing dep: %s\n", kinds[pi], mods[i].baseName, depName);
                    errors++;
                }
            }
        }
    }

    if (errors > 0) {
        // The splash was already shut down when the first error printed.
        fprintf(stderr, "%d unresolved dependency reference(s); aborting.\n", (int)errors);
        exit(1);
    }
}


int main(int argc, char *argv[]) {
    // Change to the directory containing the executable so relative
    // paths (LIBS/, WIDGETS/, APPS/, CONFIG/) resolve correctly.
    char exeDir[DVX_MAX_PATH];
    strncpy(exeDir, argv[0], sizeof(exeDir) - 1);
    exeDir[sizeof(exeDir) - 1] = '\0';

    char *sep = platformPathDirEnd(exeDir);

    if (sep) {
        *sep = '\0';
        platformChdir(exeDir);
    }

    // Truncate log, then use append-per-write
    FILE *logInit = fopen(LOG_PATH, "w");

    if (logInit) {
        fclose(logInit);
    }

    // Suppress Ctrl+C before anything else
    platformInit();

    // Switch to VGA mode 13h and show graphical splash
    platformSplashInit();
    sSplashActive = 1;
    splashDrawScreen();

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

    // Register platform + libc/libm/runtime symbols for DXE resolution
    platformRegisterDxeExports();
    dvxLog("Platform exports registered.");

    // Load all modules from libs/ and widgets/ in dependency order.
    // Each module may have a .dep file specifying load-before deps.
    // Widget modules that export wgtRegister() get it called.
    void **handles = loadAllModules();

    if (!handles || arrlen(handles) == 0) {
        splashShutdownIfActive();
        fprintf(stderr, "No modules loaded from %s/ or %s/\n", LIBS_DIR, WIDGET_DIR);
        arrfree(handles);
        return 1;
    }

    // Check if help files need recompilation (module count changed)
    helpRecompileIfNeeded();

    // Find and call shellMain from whichever module exports it
    typedef int (*ShellMainFnT)(int, char **);
    ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, "_shellMain");

    if (!shellMain) {
        dvxLog("ERROR: No module exports shellMain");

        splashShutdownIfActive();

        for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
            dlclose(handles[i]);
        }

        arrfree(handles);
        return 1;
    }

    int result = shellMain(argc, argv);

    // Clean up in reverse load order
    for (int32_t i = arrlen(handles) - 1; i >= 0; i--) {
        dlclose(handles[i]);
    }

    arrfree(handles);
    return result;
}
