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

// progman.c  -- Program Manager application for DVX Shell
//
// Displays a grid of available apps from the apps/ directory.
// Double-click or Enter launches an app.  Includes Task Manager (Ctrl+Esc).
//
// DXE App Contract:
//   This is a callback-only DXE app (hasMainLoop = false). It exports two
//   symbols: appDescriptor (metadata) and appMain (entry point). The shell
//   calls appMain once; we create windows, register callbacks, and return 0.
//   From that point on, the shell's event loop drives everything through
//   our window callbacks (onClose, onMenu, widget onClick, etc.).
//
//   Because we have no main loop, we don't need a dedicated task stack.
//   The shell runs our callbacks in task 0 during dvxUpdate().
//
//   Progman is special: it's the desktop app. It calls
//   shellRegisterDesktopUpdate() so the shell notifies us whenever an app
//   is loaded, reaped, or crashes, keeping our status bar and Task Manager
//   list current without polling.

#include "dvxApp.h"
#include "dvxDlg.h"
#include "dvxPrefs.h"
#include "dvxWgt.h"
#include "box/box.h"
#include "button/button.h"
#include "label/label.h"
#include "statusBar/statBar.h"
#include "textInput/textInpt.h"
#include "dvxWm.h"
#include "dvxPlat.h"
#include "shellApp.h"
#include "shellInf.h"
#include "dvxRes.h"
#include "imageButton/imgBtn.h"
#include "scrollPane/scrlPane.h"
#include "wrapBox/wrapBox.h"

#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <sys/stat.h>
#include "dvxMem.h"
#include "stb_ds_wrap.h"

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

// Grid layout for app buttons: 4 columns, rows created dynamically
#define PM_TOOLTIP_LEN   128
#define PM_BTN_W         100
#define PM_BTN_H         24
#define PM_CELL_W        80
#define PM_CELL_H        64
#define PM_WIN_W           440
#define PM_WIN_H           340
#define PM_GRID_SPACING    8
#define PM_SYSINFO_WIN_W   400
#define PM_SYSINFO_WIN_H   360
#define PM_APP_EXT         ".app"   // application module extension

// Menu command IDs
#define CMD_RUN          100
#define CMD_EXIT         101
#define CMD_CASCADE      200
#define CMD_TILE         201
#define CMD_TILE_H       202
#define CMD_TILE_V       203
#define CMD_MIN_ON_RUN   104
#define CMD_RESTORE_ALONE 105
#define CMD_RELOAD       106
#define CMD_ABOUT        300
#define CMD_TASK_MGR     301
#define CMD_SYSINFO      302
#define CMD_HELP         303

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

// Each discovered .app file in the apps/ directory tree
typedef struct {
    char     name[SHELL_APP_NAME_MAX]; // short display name (from "name" resource or filename)
    char     tooltip[PM_TOOLTIP_LEN];  // description for tooltip (from "description" resource)
    char     path[DVX_MAX_PATH];       // full path
    uint8_t *iconData;                 // 32x32 icon in display format (NULL if none)
    int32_t  iconW;
    int32_t  iconH;
    int32_t  iconPitch;
} AppEntryT;

// Module-level statics (s prefix). DXE apps use file-scoped statics because
// each DXE is a separate shared object with its own data segment. No risk
// of collision between apps even though the names look global.
static DxeAppContextT *sCtx         = NULL;
static AppContextT    *sAc          = NULL;
static WindowT        *sPmWindow    = NULL;
static WidgetT        *sStatusLabel = NULL;
static PrefsHandleT   *sPrefs       = NULL;
static bool            sMinOnRun      = false;
static bool            sRestoreAlone  = false;
static AppEntryT      *sAppFiles    = NULL; // stb_ds dynamic array
static int32_t         sAppCount    = 0;

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

int32_t     appMain(DxeAppContextT *ctx);
void        appShutdown(void);
static int32_t appEntryCmpName(const void *a, const void *b);
static void buildPmWindow(void);
static void desktopUpdate(void);
static void onAppButtonClick(WidgetT *w);
static void onPmClose(WindowT *win);
static void onPmMenu(WindowT *win, int32_t menuId);
static void scanAppsDir(void);
static void scanAppsDirRecurse(const char *dirPath);
static void showAboutDialog(void);
static void showSystemInfo(void);
static void updateStatusText(void);


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

// The shell reads this exported symbol to determine how to manage the app.
// hasMainLoop = false means the shell won't create a dedicated task; our
// appMain runs to completion and all subsequent work happens via callbacks.
// stackSize = 0 means "use the shell default" (irrelevant for callback apps).
AppDescriptorT appDescriptor = {
    .name        = "Program Manager",
    .hasMainLoop = false,
    .stackSize   = SHELL_STACK_DEFAULT,
    .priority    = TS_PRIORITY_NORMAL
};


void appShutdown(void) {
    shellUnregisterDesktopUpdate(desktopUpdate);
    dvxQuit(sAc);
}


// qsort comparator for AppEntryT -- case-insensitive on display name.
static int32_t appEntryCmpName(const void *a, const void *b) {
    const AppEntryT *ea = (const AppEntryT *)a;
    const AppEntryT *eb = (const AppEntryT *)b;
    return strcasecmp(ea->name, eb->name);
}


// Build the main Program Manager window with app buttons, menus, and status bar.
// Window is centered horizontally and placed in the upper quarter vertically
// so spawned app windows don't hide behind it.
static void buildPmWindow(void) {
    int32_t screenW = sAc->display.width;
    int32_t screenH = sAc->display.height;
    int32_t winW    = PM_WIN_W;
    int32_t winH    = PM_WIN_H;
    int32_t winX    = (screenW - winW) / 2;
    int32_t winY    = (screenH - winH) / 4;

    sPmWindow = dvxCreateWindow(sAc, "Program Manager", winX, winY, winW, winH, true);

    if (!sPmWindow) {
        return;
    }

    sPmWindow->onClose = onPmClose;
    sPmWindow->onMenu  = onPmMenu;

    // Menu bar
    MenuBarT *menuBar = wmAddMenuBar(sPmWindow);
    MenuT *fileMenu = wmAddMenu(menuBar, "&File");
    wmAddMenuItem(fileMenu, "&Run...", CMD_RUN);
    wmAddMenuItem(fileMenu, "Re&load", CMD_RELOAD);
    wmAddMenuSeparator(fileMenu);
    wmAddMenuItem(fileMenu, "E&xit DVX", CMD_EXIT);

    MenuT *optMenu = wmAddMenu(menuBar, "&Options");
    wmAddMenuCheckItem(optMenu, "&Minimize on Run", CMD_MIN_ON_RUN, sMinOnRun);
    wmAddMenuCheckItem(optMenu, "&Restore when Alone", CMD_RESTORE_ALONE, sRestoreAlone);

    MenuT *windowMenu = wmAddMenu(menuBar, "&Window");
    wmAddMenuItem(windowMenu, "&Cascade", CMD_CASCADE);
    wmAddMenuItem(windowMenu, "&Tile", CMD_TILE);
    wmAddMenuItem(windowMenu, "Tile &Horizontally", CMD_TILE_H);
    wmAddMenuItem(windowMenu, "Tile &Vertically", CMD_TILE_V);

    MenuT *helpMenu = wmAddMenu(menuBar, "&Help");
    wmAddMenuItem(helpMenu, "&DVX Help\tF1", CMD_HELP);
    wmAddMenuSeparator(helpMenu);
    wmAddMenuItem(helpMenu, "&About DVX...", CMD_ABOUT);
    wmAddMenuItem(helpMenu, "&System Information...", CMD_SYSINFO);
    wmAddMenuSeparator(helpMenu);
    wmAddMenuItem(helpMenu, "&Task Manager\tCtrl+Esc", CMD_TASK_MGR);

    // wgtInitWindow creates the root VBox widget that the window's content
    // area maps to. All child widgets are added to this root.
    WidgetT *root = wgtInitWindow(sAc, sPmWindow);

    // App button grid in a labeled frame. weight=100 tells the layout engine
    // this frame should consume all available vertical space (flex weight).
    WidgetT *appFrame = wgtFrame(root, "Applications");
    appFrame->weight = WGT_WEIGHT_FILL;

    if (sAppCount == 0) {
        wgtLabel(appFrame, "(No applications found in apps/ directory)");
    } else {
        // ScrollPane provides scrollbars when icons overflow.
        // WrapBox inside flows cells left-to-right, wrapping to
        // the next row when the window width is exceeded.
        WidgetT *scroll = wgtScrollPane(appFrame);
        scroll->weight = WGT_WEIGHT_FILL;
        wgtScrollPaneSetNoBorder(scroll, true);

        WidgetT *wrap = wgtWrapBox(scroll);
        wrap->weight  = WGT_WEIGHT_FILL;
        wrap->spacing = wgtPixels(PM_GRID_SPACING);
        wrap->align   = AlignCenterE;

        for (int32_t i = 0; i < sAppCount; i++) {
            WidgetT *cell = wgtVBox(wrap);
            cell->align   = AlignCenterE;
            cell->minW    = wgtPixels(PM_CELL_W);
            cell->minH    = wgtPixels(PM_CELL_H);

            WidgetT *btn = NULL;

            if (sAppFiles[i].iconData) {
                int32_t dataSize = sAppFiles[i].iconPitch * sAppFiles[i].iconH;
                uint8_t *iconCopy = (uint8_t *)malloc(dataSize);

                if (iconCopy) {
                    memcpy(iconCopy, sAppFiles[i].iconData, dataSize);
                    btn = wgtImageButton(cell, iconCopy, sAppFiles[i].iconW, sAppFiles[i].iconH, sAppFiles[i].iconPitch);
                    btn->maxW = wgtPixels(sAppFiles[i].iconW + 4);
                    btn->maxH = wgtPixels(sAppFiles[i].iconH + 4);
                }
            }

            if (!btn) {
                btn = wgtButton(cell, sAppFiles[i].name);
                btn->prefW = wgtPixels(PM_BTN_W);
                btn->prefH = wgtPixels(PM_BTN_H);
            }

            btn->userData = &sAppFiles[i];
            btn->onClick  = onAppButtonClick;

            if (sAppFiles[i].tooltip[0]) {
                wgtSetTooltip(btn, sAppFiles[i].tooltip);
            }

            WidgetT *label = wgtLabel(cell, sAppFiles[i].name);
            label->maxW = wgtPixels(PM_CELL_W);
            wgtLabelSetAlign(label, AlignCenterE);
        }
    }

    // Status bar at bottom; weight=100 on the label makes it fill the bar
    // width so text can be left-aligned naturally.
    WidgetT *statusBar = wgtStatusBar(root);
    sStatusLabel = wgtLabel(statusBar, "");
    sStatusLabel->weight = WGT_WEIGHT_FILL;
    updateStatusText();

}


// Shell calls this via shellRegisterDesktopUpdate whenever an app is loaded,
// reaped, or crashes. We refresh the running count in the status bar.
// (Task Manager refresh is handled by the shell's shellDesktopUpdate.)
static void desktopUpdate(void) {
    updateStatusText();

    // Auto-restore if we're the only running app and minimized
    if (sRestoreAlone && sPmWindow && sPmWindow->minimized) {
        if (shellRunningAppCount() <= 1) {
            wmRestoreMinimized(&sAc->stack, &sAc->dirty, &sAc->display, sPmWindow);
        }
    }
}


// Widget click handler for app grid buttons. userData was set to the
// AppEntryT pointer during window construction, giving us the .app path.
static void onAppButtonClick(WidgetT *w) {
    AppEntryT *entry = (AppEntryT *)w->userData;

    if (!entry) {
        return;
    }

    shellLoadApp(sAc, entry->path);
    updateStatusText();

    if (sMinOnRun && sPmWindow) {
        dvxMinimizeWindow(sAc, sPmWindow);
    }
}


// Closing the Program Manager is equivalent to shutting down the entire shell.
// dvxQuit() signals the main event loop to exit, which triggers
// shellTerminateAllApps() to gracefully tear down all loaded DXEs.
static void onPmClose(WindowT *win) {
    (void)win;
    int32_t result = dvxMessageBox(sAc, "Exit DVX", "Are you sure you want to exit DVX?", MB_YESNO | MB_ICONQUESTION);

    if (result == ID_YES) {
        dvxQuit(sAc);
    }
}


static void onPmMenu(WindowT *win, int32_t menuId) {
    (void)win;

    switch (menuId) {
        case CMD_RUN:
            {
                FileFilterT filters[] = {
                    { "Applications (*.app)" },
                    { "All Files (*.*)" }
                };
                char path[DVX_MAX_PATH];

                if (dvxFileDialog(sAc, "Run Application", FD_OPEN, "apps", filters, 2, path, sizeof(path))) {
                    shellLoadApp(sAc, path);
                    updateStatusText();

                    if (sMinOnRun && sPmWindow) {
                        dvxMinimizeWindow(sAc, sPmWindow);
                    }
                }
            }
            break;

        case CMD_RELOAD:
            dvxDestroyWindow(sAc, sPmWindow);
            sPmWindow    = NULL;
            sStatusLabel = NULL;
            scanAppsDir();
            buildPmWindow();
            break;

        case CMD_EXIT:
            onPmClose(sPmWindow);
            break;

        case CMD_CASCADE:
            dvxCascadeWindows(sAc);
            break;

        case CMD_TILE:
            dvxTileWindows(sAc);
            break;

        case CMD_TILE_H:
            dvxTileWindowsH(sAc);
            break;

        case CMD_TILE_V:
            dvxTileWindowsV(sAc);
            break;

        case CMD_MIN_ON_RUN:
            sMinOnRun = !sMinOnRun;
            shellEnsureConfigDir(sCtx);
            prefsSetBool(sPrefs, "options", "minimizeOnRun", sMinOnRun);
            prefsSave(sPrefs);
            break;

        case CMD_RESTORE_ALONE:
            sRestoreAlone = !sRestoreAlone;
            shellEnsureConfigDir(sCtx);
            prefsSetBool(sPrefs, "options", "restoreAlone", sRestoreAlone);
            prefsSave(sPrefs);
            break;

        case CMD_HELP: {
            char viewerPath[DVX_MAX_PATH];
            char sysHlp[DVX_MAX_PATH];
            snprintf(viewerPath, sizeof(viewerPath), "APPS" DVX_PATH_SEP "KPUNCH" DVX_PATH_SEP "DVXHELP" DVX_PATH_SEP "DVXHELP.APP");
            snprintf(sysHlp, sizeof(sysHlp), "%s" DVX_PATH_SEP "%s", sCtx->appDir, "dvxhelp.hlp");
            shellLoadAppWithArgs(sAc, viewerPath, sysHlp);
            break;
        }

        case CMD_ABOUT:
            showAboutDialog();
            break;

        case CMD_SYSINFO:
            showSystemInfo();
            break;

        case CMD_TASK_MGR:
            if (shellCtrlEscFn) {
                shellCtrlEscFn(sAc);
            }
            break;
    }
}


// Top-level scan entry point. Recursively walks apps/ looking for .app files.
// The apps/ path is relative to the working directory, which the shell sets
// to the root of the DVX install before loading any apps.
static void scanAppsDir(void) {
    // scanAppsDirRecurse pumps dvxUpdate(), which could re-enter a rescan and
    // free sAppFiles mid-iteration.  Make the existing structural safety
    // explicit with a reentrancy guard.
    static bool sScanning = false;

    if (sScanning) {
        return;
    }

    sScanning = true;

    // Free icons from previous scan
    for (int32_t i = 0; i < sAppCount; i++) {
        free(sAppFiles[i].iconData);
    }

    arrfree(sAppFiles);
    sAppFiles = NULL;
    sAppCount = 0;
    scanAppsDirRecurse("apps");

    if (sAppCount > 1) {
        qsort(sAppFiles, sAppCount, sizeof(AppEntryT), (int (*)(const void *, const void *))appEntryCmpName);
    }

    dvxLog("Progman: found %ld app(s)", (long)sAppCount);
    sScanning = false;
}


// Recursive directory walker. Subdirectories under apps/ allow organizing
// apps (e.g., apps/games/, apps/tools/). Each .app file is a DXE3 shared
// object that the shell can dlopen(). We skip progman.app to avoid listing
// ourselves in the launcher grid.
static void scanAppsDirRecurse(const char *dirPath) {
    // Collect all entries first, close the handle, then process.
    // DOS has limited file handles; keeping a DIR open while
    // opening .app files for resource loading causes failures.
    char **names = dvxReadDir(dirPath);

    if (!names) {
        if (sAppCount == 0) {
            dvxLog("Progman: %s directory not found", dirPath);
        }

        return;
    }

    int32_t nEntries = (int32_t)arrlen(names);

    for (int32_t i = 0; i < nEntries; i++) {
        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 && S_ISDIR(st.st_mode)) {
            scanAppsDirRecurse(fullPath);
            continue;
        }

        if (!dvxHasExt(names[i], PM_APP_EXT) || strcasecmp(names[i], "progman.app") == 0) {
            continue;
        }

        AppEntryT newEntry;
        memset(&newEntry, 0, sizeof(newEntry));
        snprintf(newEntry.path, sizeof(newEntry.path), "%s", fullPath);

        // Default name from filename (without .app extension)
        int32_t len     = (int32_t)strlen(names[i]);
        int32_t nameLen = len - (int32_t)strlen(PM_APP_EXT);

        if (nameLen >= SHELL_APP_NAME_MAX) {
            nameLen = SHELL_APP_NAME_MAX - 1;
        }

        memcpy(newEntry.name, names[i], nameLen);
        newEntry.name[nameLen] = '\0';

        newEntry.name[0] = (char)toupper((unsigned char)newEntry.name[0]);

        // Override from embedded resources if available
        newEntry.iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &newEntry.iconW, &newEntry.iconH, &newEntry.iconPitch);

        if (!newEntry.iconData) {
            dvxLog("Progman: no icon32 resource in %s", names[i]);
        }

        dvxResLoadText(fullPath, "name", newEntry.name, SHELL_APP_NAME_MAX);
        dvxResLoadText(fullPath, "description", newEntry.tooltip, sizeof(newEntry.tooltip));

        dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, names[i], newEntry.iconData ? "yes" : "no");
        arrput(sAppFiles, newEntry);
        sAppCount = (int32_t)arrlen(sAppFiles);

        dvxUpdate(sAc);
    }

    dvxReadDirFree(names);
}


static void showAboutDialog(void) {
    dvxMessageBox(sAc, "About DVX",
        "DVX 1.0 - \"DOS Visual eXecutive\" GUI System.\n\n\"We have Windows at home.\"\n\nCopyright 2026 Scott Duensing\nKangaroo Punch Studios\nhttps://kangaroopunch.com",
        MB_OK | MB_ICONINFO);
}


static void showSystemInfo(void) {
    const char *info = shellGetSystemInfo();

    if (!info || !info[0]) {
        dvxInfoBox(sAc, "System Information", "No system information available.");
        return;
    }

    // Create a window with a read-only text area
    int32_t screenW = sAc->display.width;
    int32_t screenH = sAc->display.height;
    int32_t winW    = PM_SYSINFO_WIN_W;
    int32_t winH    = PM_SYSINFO_WIN_H;
    int32_t winX    = (screenW - winW) / 2;
    int32_t winY    = (screenH - winH) / 4;

    WindowT *win = dvxCreateWindow(sAc, "System Information", winX, winY, winW, winH, true);

    if (!win) {
        return;
    }

    WidgetT *root = wgtInitWindow(sAc, win);
    WidgetT *ta   = wgtTextArea(root, 4096);
    ta->weight = WGT_WEIGHT_FILL;
    wgtSetText(ta, info);

    // Don't disable  -- wgtSetEnabled(false) blocks all input including scrollbar
    wgtSetReadOnly(ta, true);
}


static void updateStatusText(void) {
    if (!sStatusLabel) {
        return;
    }

    static char buf[64];
    int32_t count = shellRunningAppCount();

    if (count == 0) {
        snprintf(buf, sizeof(buf), "No applications running");
    } else if (count == 1) {
        snprintf(buf, sizeof(buf), "1 application running");
    } else {
        snprintf(buf, sizeof(buf), "%ld applications running", (long)count);
    }

    wgtSetText(sStatusLabel, buf);
}


// The shell calls appMain exactly once after dlopen() resolves our symbols.
// We scan for apps, build the UI, register our desktop update callback, then
// return 0 (success). From here on the shell drives us through callbacks.
// Returning non-zero would signal a load failure and the shell would unload us.
int32_t appMain(DxeAppContextT *ctx) {
    sCtx     = ctx;
    sAc      = ctx->shellCtx;

    // Set help file for F1 — system help lives in progman's app directory
    snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%s", ctx->appDir, "dvxhelp.hlp");

    // Load saved preferences
    char prefsPath[DVX_MAX_PATH];
    shellConfigPath(sCtx, "progman.ini", prefsPath, sizeof(prefsPath));
    sPrefs    = prefsLoad(prefsPath);
    sMinOnRun     = prefsGetBool(sPrefs, "options", "minimizeOnRun", false);
    sRestoreAlone = prefsGetBool(sPrefs, "options", "restoreAlone", false);

    scanAppsDir();
    buildPmWindow();

    // Register for state change notifications from the shell so our status
    // bar and Task Manager stay current without polling
    shellRegisterDesktopUpdate(desktopUpdate);



    return 0;
}
