// ideProject.c -- DVX BASIC project file management and project window
//
// The .dbp (DVX BASIC Project) file is INI-format:
//
//   [Project]
//   Name = MyProject
//
//   [Modules]
//   Count = 2
//   File0 = MAIN.BAS
//   File1 = UTILS.BAS
//
//   [Forms]
//   Count = 1
//   File0 = FORM1.FRM
//
//   [Settings]
//   StartupForm = Form1
//
// All file paths are relative to the directory containing the .dbp file.
// Uses the handle-based dvxPrefs API with a dedicated handle per load/save
// so project files don't interfere with the IDE's own preferences.

#include "ideProject.h"
#include "dvxApp.h"
#include "dvxDialog.h"
#include "dvxPrefs.h"
#include "dvxWm.h"
#include "widgetBox.h"
#include "widgetButton.h"
#include "widgetDropdown.h"
#include "widgetImage.h"
#include "widgetLabel.h"
#include "widgetTextInput.h"
#include "widgetTreeView.h"

#include "thirdparty/stb_ds_wrap.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

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

#define PRJ_WIN_W       180
#define PRJ_WIN_H       300
#define PRJ_MAX_FILES   256

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

static PrjStateT       *sPrj      = NULL;
static WindowT         *sPrjWin   = NULL;
static WidgetT         *sTree     = NULL;
static PrjFileClickFnT  sOnClick     = NULL;
static PrjSelChangeFnT  sOnSelChange = NULL;
static char            **sLabels  = NULL; // stb_ds array of strdup'd strings

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

static void onPrjWinClose(WindowT *win);
static void onTreeItemDblClick(WidgetT *w);
static void onTreeSelChanged(WidgetT *w);
static bool validateIcon(const char *fullPath, bool showErrors);

// ============================================================
// prjInit
// ============================================================

void prjInit(PrjStateT *prj) {
    memset(prj, 0, sizeof(*prj));
    prj->activeFileIdx = -1;
}


// ============================================================
// prjClose
// ============================================================

void prjClose(PrjStateT *prj) {
    for (int32_t i = 0; i < prj->fileCount; i++) {
        free(prj->files[i].buffer);
    }

    arrfree(prj->files);
    arrfree(prj->sourceMap);
    prjInit(prj);
}


// ============================================================
// prjLoad
// ============================================================

bool prjLoad(PrjStateT *prj, const char *dbpPath) {
    PrefsHandleT *h = prefsLoad(dbpPath);

    if (!h) {
        return false;
    }

    prjInit(prj);
    snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);

    // Derive project directory
    snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
    char *sep = strrchr(prj->projectDir, '/');
    char *sep2 = strrchr(prj->projectDir, '\\');

    if (sep2 > sep) {
        sep = sep2;
    }

    if (sep) {
        *sep = '\0';
    } else {
        prj->projectDir[0] = '.';
        prj->projectDir[1] = '\0';
    }

    // [Project] section
    const char *val;

    val = prefsGetString(h, "Project", "Name", NULL);
    if (val) { snprintf(prj->name, sizeof(prj->name), "%s", val); }

    val = prefsGetString(h, "Project", "Author", NULL);
    if (val) { snprintf(prj->author, sizeof(prj->author), "%s", val); }

    val = prefsGetString(h, "Project", "Company", NULL);
    if (val) { snprintf(prj->company, sizeof(prj->company), "%s", val); }

    val = prefsGetString(h, "Project", "Version", NULL);
    if (val) { snprintf(prj->version, sizeof(prj->version), "%s", val); }

    val = prefsGetString(h, "Project", "Copyright", NULL);
    if (val) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", val); }

    val = prefsGetString(h, "Project", "Description", NULL);
    if (val) { snprintf(prj->description, sizeof(prj->description), "%s", val); }

    val = prefsGetString(h, "Project", "Icon", NULL);
    if (val) { snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", val); }

    // [Modules] section -- File0, File1, ...
    for (int32_t i = 0; i < PRJ_MAX_FILES; i++) {
        char key[16];
        snprintf(key, sizeof(key), "File%d", (int)i);
        val = prefsGetString(h, "Modules", key, NULL);

        if (!val) {
            break;
        }

        prjAddFile(prj, val, false);
    }

    // [Forms] section -- File0, File1, ...
    for (int32_t i = 0; i < PRJ_MAX_FILES; i++) {
        char key[16];
        snprintf(key, sizeof(key), "File%d", (int)i);
        val = prefsGetString(h, "Forms", key, NULL);

        if (!val) {
            break;
        }

        prjAddFile(prj, val, true);
    }

    // [Settings] section
    val = prefsGetString(h, "Settings", "StartupForm", NULL);
    if (val) { snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", val); }

    prefsClose(h);
    prj->dirty = false;
    return true;
}


// ============================================================
// prjSave
// ============================================================

bool prjSave(const PrjStateT *prj) {
    if (prj->projectPath[0] == '\0') {
        return false;
    }

    PrefsHandleT *h = prefsCreate();

    if (!h) {
        return false;
    }

    // [Project] section
    prefsSetString(h, "Project", "Name", prj->name);

    if (prj->author[0])      { prefsSetString(h, "Project", "Author", prj->author); }
    if (prj->company[0])     { prefsSetString(h, "Project", "Company", prj->company); }
    if (prj->version[0])     { prefsSetString(h, "Project", "Version", prj->version); }
    if (prj->copyright[0])   { prefsSetString(h, "Project", "Copyright", prj->copyright); }
    if (prj->description[0]) { prefsSetString(h, "Project", "Description", prj->description); }
    if (prj->iconPath[0])    { prefsSetString(h, "Project", "Icon", prj->iconPath); }

    // [Modules] section
    int32_t modIdx = 0;

    for (int32_t i = 0; i < prj->fileCount; i++) {
        if (!prj->files[i].isForm) {
            char key[16];
            snprintf(key, sizeof(key), "File%d", (int)modIdx++);
            prefsSetString(h, "Modules", key, prj->files[i].path);
        }
    }

    prefsSetInt(h, "Modules", "Count", modIdx);

    // [Forms] section
    int32_t frmIdx = 0;

    for (int32_t i = 0; i < prj->fileCount; i++) {
        if (prj->files[i].isForm) {
            char key[16];
            snprintf(key, sizeof(key), "File%d", (int)frmIdx++);
            prefsSetString(h, "Forms", key, prj->files[i].path);
        }
    }

    prefsSetInt(h, "Forms", "Count", frmIdx);

    // [Settings] section
    prefsSetString(h, "Settings", "StartupForm", prj->startupForm);

    bool ok = prefsSaveAs(h, prj->projectPath);
    prefsClose(h);
    return ok;
}


// ============================================================
// prjSaveAs
// ============================================================

bool prjSaveAs(PrjStateT *prj, const char *dbpPath) {
    snprintf(prj->projectPath, sizeof(prj->projectPath), "%s", dbpPath);

    // Update project directory
    snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", dbpPath);
    char *sep = strrchr(prj->projectDir, '/');
    char *sep2 = strrchr(prj->projectDir, '\\');

    if (sep2 > sep) {
        sep = sep2;
    }

    if (sep) {
        *sep = '\0';
    } else {
        prj->projectDir[0] = '.';
        prj->projectDir[1] = '\0';
    }

    return prjSave(prj);
}


// ============================================================
// prjNew
// ============================================================

void prjNew(PrjStateT *prj, const char *name, const char *directory, PrefsHandleT *prefs) {
    prjInit(prj);
    snprintf(prj->name, sizeof(prj->name), "%s", name);
    snprintf(prj->projectDir, sizeof(prj->projectDir), "%s", directory);
    snprintf(prj->projectPath, sizeof(prj->projectPath), "%s/%s.dbp", directory, name);
    prj->dirty = true;

    // Apply defaults from preferences
    if (prefs) {
        snprintf(prj->author, sizeof(prj->author), "%s", prefsGetString(prefs, "defaults", "author", ""));
        snprintf(prj->company, sizeof(prj->company), "%s", prefsGetString(prefs, "defaults", "company", ""));
        snprintf(prj->version, sizeof(prj->version), "%s", prefsGetString(prefs, "defaults", "version", "1.0"));
        snprintf(prj->copyright, sizeof(prj->copyright), "%s", prefsGetString(prefs, "defaults", "copyright", ""));
        snprintf(prj->description, sizeof(prj->description), "%s", prefsGetString(prefs, "defaults", "description", ""));
    }
}


// ============================================================
// prjAddFile
// ============================================================

int32_t prjAddFile(PrjStateT *prj, const char *relativePath, bool isForm) {
    PrjFileT entry;
    memset(&entry, 0, sizeof(entry));
    snprintf(entry.path, sizeof(entry.path), "%s", relativePath);
    entry.isForm = isForm;
    arrput(prj->files, entry);
    prj->fileCount = (int32_t)arrlen(prj->files);
    prj->dirty = true;
    return prj->fileCount - 1;
}


// ============================================================
// prjLoadAllFiles -- read all project files into memory buffers
// and extract form names from .frm files.
// ============================================================

void prjLoadAllFiles(PrjStateT *prj, AppContextT *ctx) {
    for (int32_t i = 0; i < prj->fileCount; i++) {
        if (prj->files[i].buffer) {
            continue; // already loaded
        }

        char fullPath[DVX_MAX_PATH];
        prjFullPath(prj, i, fullPath, sizeof(fullPath));

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

        if (!f) {
            continue;
        }

        fseek(f, 0, SEEK_END);
        long size = ftell(f);
        fseek(f, 0, SEEK_SET);

        if (size <= 0) {
            fclose(f);
            continue;
        }

        char *buf = (char *)malloc(size + 1);

        if (!buf) {
            fclose(f);
            continue;
        }

        int32_t bytesRead = (int32_t)fread(buf, 1, size, f);
        fclose(f);
        buf[bytesRead] = '\0';

        prj->files[i].buffer = buf;

        // Extract form name from .frm files
        if (prj->files[i].isForm) {
            const char *pos = buf;

            while (*pos) {
                while (*pos == ' ' || *pos == '\t') { pos++; }

                if (strncasecmp(pos, "Begin Form ", 11) == 0) {
                    const char *np = pos + 11;
                    while (*np == ' ' || *np == '\t') { np++; }
                    int32_t n = 0;
                    while (*np && *np != ' ' && *np != '\t' && *np != '\r' && *np != '\n' && n < PRJ_MAX_NAME - 1) {
                        prj->files[i].formName[n++] = *np++;
                    }
                    prj->files[i].formName[n] = '\0';
                    break;
                }

                while (*pos && *pos != '\n') { pos++; }
                if (*pos == '\n') { pos++; }
            }
        }

        // Yield between files to keep the UI responsive
        if (ctx) {
            dvxUpdate(ctx);
        }
    }
}


// ============================================================
// prjRemoveFile
// ============================================================

void prjRemoveFile(PrjStateT *prj, int32_t idx) {
    if (idx < 0 || idx >= prj->fileCount) {
        return;
    }

    free(prj->files[idx].buffer);
    arrdel(prj->files, idx);
    prj->fileCount = (int32_t)arrlen(prj->files);

    // Adjust active file index
    if (prj->activeFileIdx == idx) {
        prj->activeFileIdx = -1;
    } else if (prj->activeFileIdx > idx) {
        prj->activeFileIdx--;
    }

    prj->dirty = true;
}


// ============================================================
// prjFullPath
// ============================================================

void prjFullPath(const PrjStateT *prj, int32_t fileIdx, char *outPath, int32_t outSize) {
    if (fileIdx < 0 || fileIdx >= prj->fileCount) {
        outPath[0] = '\0';
        return;
    }

    snprintf(outPath, outSize, "%s/%s", prj->projectDir, prj->files[fileIdx].path);
}


// ============================================================
// prjMapLine
// ============================================================

bool prjMapLine(const PrjStateT *prj, int32_t concatLine, int32_t *outFileIdx, int32_t *outLocalLine) {
    for (int32_t i = 0; i < prj->sourceMapCount; i++) {
        const PrjSourceMapT *m = &prj->sourceMap[i];

        if (concatLine >= m->startLine && concatLine < m->startLine + m->lineCount) {
            *outFileIdx  = m->fileIdx;
            *outLocalLine = concatLine - m->startLine + 1;
            return true;
        }
    }

    return false;
}


// ============================================================
// Project window callbacks
// ============================================================

static void onPrjWinClose(WindowT *win) {
    (void)win;
}


int32_t prjGetSelectedFileIdx(void) {
    if (!sTree) {
        return -1;
    }

    WidgetT *sel = wgtTreeViewGetSelected(sTree);

    if (!sel) {
        return -1;
    }

    return (int32_t)(intptr_t)sel->userData;
}


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

    if (sOnSelChange) {
        sOnSelChange();
    }
}


static void onTreeItemDblClick(WidgetT *w) {
    if (!sPrj || !sOnClick) {
        return;
    }

    int32_t fileIdx = (int32_t)(intptr_t)w->userData;

    if (fileIdx >= 0 && fileIdx < sPrj->fileCount) {
        sOnClick(fileIdx, sPrj->files[fileIdx].isForm);
    }
}


// ============================================================
// prjCreateWindow
// ============================================================

WindowT *prjCreateWindow(AppContextT *ctx, PrjStateT *prj, PrjFileClickFnT onClick, PrjSelChangeFnT onSelChange) {
    sPrj         = prj;
    sOnClick     = onClick;
    sOnSelChange = onSelChange;

    sPrjWin = dvxCreateWindow(ctx, "Project", 0, 250, PRJ_WIN_W, PRJ_WIN_H, true);

    if (!sPrjWin) {
        return NULL;
    }

    sPrjWin->onClose = onPrjWinClose;

    WidgetT *root = wgtInitWindow(ctx, sPrjWin);
    sTree = wgtTreeView(root);
    sTree->weight   = 100;
    sTree->onChange  = onTreeSelChanged;

    prjRebuildTree(prj);
    return sPrjWin;
}


// ============================================================
// prjDestroyWindow
// ============================================================

void prjDestroyWindow(AppContextT *ctx, WindowT *win) {
    if (win) {
        dvxDestroyWindow(ctx, win);
    }

    // Free label strings
    if (sLabels) {
        for (int32_t i = 0; i < (int32_t)arrlen(sLabels); i++) {
            free(sLabels[i]);
        }

        arrfree(sLabels);
        sLabels = NULL;
    }

    sPrjWin  = NULL;
    sTree    = NULL;
    sPrj     = NULL;
    sOnClick = NULL;
}


// ============================================================
// prjRebuildTree
// ============================================================

void prjRebuildTree(PrjStateT *prj) {
    if (!sTree) {
        return;
    }

    // Clear existing items by removing all children
    sTree->firstChild = NULL;
    sTree->lastChild  = NULL;

    // Free old labels
    if (sLabels) {
        for (int32_t i = 0; i < (int32_t)arrlen(sLabels); i++) {
            free(sLabels[i]);
        }

        arrfree(sLabels);
        sLabels = NULL;
    }

    if (!prj || prj->fileCount == 0) {
        return;
    }

    // Project name as root
    char *projLabel = strdup(prj->name[0] ? prj->name : "Project");
    arrput(sLabels, projLabel);
    WidgetT *projNode = wgtTreeItem(sTree, projLabel);
    projNode->userData = (void *)(intptr_t)-1;
    wgtTreeItemSetExpanded(projNode, true);

    // Forms and Modules groups
    char *formsLabel = strdup("Forms");
    arrput(sLabels, formsLabel);
    WidgetT *formsNode = wgtTreeItem(projNode, formsLabel);
    formsNode->userData = (void *)(intptr_t)-1;
    wgtTreeItemSetExpanded(formsNode, true);

    char *modsLabel = strdup("Modules");
    arrput(sLabels, modsLabel);
    WidgetT *modsNode = wgtTreeItem(projNode, modsLabel);
    modsNode->userData = (void *)(intptr_t)-1;
    wgtTreeItemSetExpanded(modsNode, true);

    for (int32_t i = 0; i < prj->fileCount; i++) {
        char buf[DVX_MAX_PATH + 4];
        snprintf(buf, sizeof(buf), "%s%s", prj->files[i].path, prj->files[i].modified ? " *" : "");
        char *label = strdup(buf);
        arrput(sLabels, label);
        WidgetT *item = wgtTreeItem(prj->files[i].isForm ? formsNode : modsNode, label);
        item->userData   = (void *)(intptr_t)i;
        item->onDblClick = onTreeItemDblClick;
    }

    wgtInvalidate(sTree);
}


// ============================================================
// Project properties dialog
// ============================================================

#define PPD_WIDTH    380
#define PPD_LABEL_W  96
#define PPD_BTN_W    70
#define PPD_BTN_H    24
#define PPD_DESC_H   60

static struct {
    bool          done;
    bool          accepted;
    WidgetT      *name;
    WidgetT      *author;
    WidgetT      *company;
    WidgetT      *version;
    WidgetT      *copyright;
    WidgetT      *description;
    WidgetT      *startupForm;
    const char  **formNames;    // stb_ds array of form name strings for startup dropdown
    WidgetT      *iconPreview;
    char          iconPath[DVX_MAX_PATH];
    const char   *appPath;
    AppContextT  *ctx;
    PrjStateT    *prj;
} sPpd;

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

    // Validate icon path if set
    if (sPpd.iconPath[0] && sPpd.prj) {
        char fullPath[DVX_MAX_PATH * 2];
        snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, sPpd.iconPath);

        if (!validateIcon(fullPath, true)) {
            return;
        }
    }

    sPpd.accepted = true;
    sPpd.done     = true;
}
static void ppdOnCancel(WidgetT *w)  { (void)w; sPpd.accepted = false; sPpd.done = true; }
static void ppdOnClose(WindowT *win) { (void)win; sPpd.accepted = false; sPpd.done = true; }


// validateIcon -- check that an image file is a valid 32x32 icon.
// Returns true if valid.  Shows an error dialog and returns false if not.

static bool validateIcon(const char *fullPath, bool showErrors) {
    int32_t infoW = 0;
    int32_t infoH = 0;

    if (!dvxImageInfo(fullPath, &infoW, &infoH)) {
        if (showErrors) {
            dvxMessageBox(sPpd.ctx, "Invalid Icon", "Could not read image file.", MB_OK | MB_ICONERROR);
        }
        return false;
    }

    if (infoW != 32 || infoH != 32) {
        if (showErrors) {
            char msg[128];
            snprintf(msg, sizeof(msg), "Icon must be 32x32 pixels.\nThis image is %dx%d.", (int)infoW, (int)infoH);
            dvxMessageBox(sPpd.ctx, "Invalid Icon", msg, MB_OK | MB_ICONWARNING);
        }
        return false;
    }

    return true;
}


static void ppdLoadIconPreview(void) {
    if (!sPpd.iconPreview || !sPpd.ctx || !sPpd.prj) {
        return;
    }

    if (!sPpd.iconPath[0]) {
        return;
    }

    const char *relPath = sPpd.iconPath;

    char fullPath[DVX_MAX_PATH * 2];
    snprintf(fullPath, sizeof(fullPath), "%s/%s", sPpd.prj->projectDir, relPath);

    if (!validateIcon(fullPath, true)) {
        sPpd.iconPath[0] = '\0';
        return;
    }

    int32_t w     = 0;
    int32_t h     = 0;
    int32_t pitch = 0;
    uint8_t *data = dvxLoadImage(sPpd.ctx, fullPath, &w, &h, &pitch);

    if (data) {
        wgtImageSetData(sPpd.iconPreview, data, w, h, pitch);
    }
}


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

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

    char path[DVX_MAX_PATH];

    if (dvxFileDialog(sPpd.ctx, "Select Icon", FD_OPEN, NULL, filters, 2, path, sizeof(path))) {
        if (!validateIcon(path, true)) {
            return;
        }

        // The icon must be in the project directory so the relative
        // path works when the project is reloaded.
        const char *relPath = NULL;
        int32_t dirLen = (int32_t)strlen(sPpd.prj->projectDir);

        if (strncasecmp(path, sPpd.prj->projectDir, dirLen) == 0 &&
            (path[dirLen] == '/' || path[dirLen] == '\\')) {
            relPath = path + dirLen + 1;
        }

        if (!relPath) {
            int32_t result = dvxMessageBox(sPpd.ctx, "Copy Icon",
                "The icon is outside the project directory.\nCopy it to the project?",
                MB_YESNO | MB_ICONQUESTION);

            if (result != ID_YES) {
                return;
            }

            // Get just the filename
            const char *fname = strrchr(path, '/');
            const char *fname2 = strrchr(path, '\\');

            if (fname2 > fname) {
                fname = fname2;
            }

            fname = fname ? fname + 1 : path;

            // Check if destination already exists
            char destPath[DVX_MAX_PATH * 2];
            snprintf(destPath, sizeof(destPath), "%s/%s", sPpd.prj->projectDir, fname);

            FILE *existing = fopen(destPath, "rb");

            if (existing) {
                fclose(existing);

                char msg[DVX_MAX_PATH + 32];
                snprintf(msg, sizeof(msg), "%s already exists.\nOverwrite it?", fname);
                int32_t ow = dvxMessageBox(sPpd.ctx, "Overwrite", msg, MB_YESNO | MB_ICONQUESTION);

                if (ow != ID_YES) {
                    return;
                }
            }

            // Copy the file
            FILE *src = fopen(path, "rb");

            if (!src) {
                dvxMessageBox(sPpd.ctx, "Error", "Could not read source file.", MB_OK | MB_ICONERROR);
                return;
            }

            FILE *dst = fopen(destPath, "wb");

            if (!dst) {
                fclose(src);
                dvxMessageBox(sPpd.ctx, "Error", "Could not write to project directory.", MB_OK | MB_ICONERROR);
                return;
            }

            char buf[4096];
            size_t n;

            while ((n = fread(buf, 1, sizeof(buf), src)) > 0) {
                fwrite(buf, 1, n, dst);
            }

            fclose(src);
            fclose(dst);

            relPath = fname;
        }

        snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", relPath);
        ppdLoadIconPreview();
    }
}


static WidgetT *ppdAddRow(WidgetT *parent, const char *labelText, const char *value, int32_t maxLen) {
    WidgetT *row = wgtHBox(parent);
    row->spacing = wgtPixels(4);

    WidgetT *lbl = wgtLabel(row, labelText);
    lbl->minW = wgtPixels(PPD_LABEL_W);

    WidgetT *input = wgtTextInput(row, maxLen);
    input->weight = 100;
    wgtSetText(input, value);

    return input;
}


bool prjPropertiesDialog(AppContextT *ctx, PrjStateT *prj, const char *appPath) {
    if (!ctx || !prj) {
        return false;
    }

    WindowT *win = dvxCreateWindowCentered(ctx, "Project Properties", PPD_WIDTH, 380, false);

    if (!win) {
        return false;
    }

    win->modal   = true;
    win->onClose = ppdOnClose;
    win->maxW    = win->w;
    win->maxH    = win->h;

    sPpd.done     = false;
    sPpd.accepted = false;
    sPpd.ctx      = ctx;
    sPpd.prj      = prj;
    sPpd.appPath  = appPath;

    WidgetT *root = wgtInitWindow(ctx, win);

    if (!root) {
        dvxDestroyWindow(ctx, win);
        return false;
    }

    root->spacing = wgtPixels(2);

    sPpd.name      = ppdAddRow(root, "Name:",      prj->name,      PRJ_MAX_NAME);
    sPpd.author    = ppdAddRow(root, "Author:",    prj->author,    PRJ_MAX_STRING);
    sPpd.company   = ppdAddRow(root, "Company:",   prj->company,   PRJ_MAX_STRING);
    sPpd.version   = ppdAddRow(root, "Version:",   prj->version,   PRJ_MAX_NAME);
    sPpd.copyright = ppdAddRow(root, "Copyright:", prj->copyright, PRJ_MAX_STRING);

    // Startup form dropdown
    {
        WidgetT *sfRow = wgtHBox(root);
        sfRow->spacing = wgtPixels(4);

        WidgetT *sfLbl = wgtLabel(sfRow, "Startup Form:");
        sfLbl->minW = wgtPixels(PPD_LABEL_W);

        sPpd.startupForm = wgtDropdown(sfRow);
        sPpd.startupForm->weight = 100;

        // Populate with form names from the project
        sPpd.formNames = NULL;
        int32_t selectedIdx = 0;

        for (int32_t i = 0; i < prj->fileCount; i++) {
            if (!prj->files[i].isForm) {
                continue;
            }

            // Use the cached form object name, fall back to filename
            const char *name = prj->files[i].formName;
            char fallback[PRJ_MAX_NAME];

            if (!name[0]) {
                snprintf(fallback, sizeof(fallback), "%s", prj->files[i].path);
                char *dot = strrchr(fallback, '.');
                if (dot) { *dot = '\0'; }
                name = fallback;
            }

            arrput(sPpd.formNames, strdup(name));

            if (strcasecmp(name, prj->startupForm) == 0) {
                selectedIdx = (int32_t)arrlen(sPpd.formNames) - 1;
            }
        }

        int32_t formCount = (int32_t)arrlen(sPpd.formNames);
        wgtDropdownSetItems(sPpd.startupForm, sPpd.formNames, formCount);

        if (formCount > 0) {
            wgtDropdownSetSelected(sPpd.startupForm, selectedIdx);
        }
    }

    // Icon row: label + preview + Browse button
    {
        WidgetT *iconRow = wgtHBox(root);
        iconRow->spacing = wgtPixels(4);

        WidgetT *iconLbl = wgtLabel(iconRow, "Icon:");
        iconLbl->minW = wgtPixels(PPD_LABEL_W);

        // Load "noicon" placeholder from app resources
        int32_t niW = 0;
        int32_t niH = 0;
        int32_t niP = 0;
        uint8_t *noIconData = appPath ? dvxResLoadIcon(ctx, appPath, "noicon", &niW, &niH, &niP) : NULL;

        if (noIconData) {
            sPpd.iconPreview = wgtImage(iconRow, noIconData, niW, niH, niP);
        } else {
            uint8_t *placeholder = (uint8_t *)calloc(4, 1);
            sPpd.iconPreview = wgtImage(iconRow, placeholder, 1, 1, 4);
        }

        WidgetT *browseBtn = wgtButton(iconRow, "Browse...");
        browseBtn->onClick = ppdOnBrowseIcon;

        snprintf(sPpd.iconPath, sizeof(sPpd.iconPath), "%s", prj->iconPath);
        ppdLoadIconPreview();
    }

    // Description: label above, textarea below (matches Preferences layout)
    wgtLabel(root, "Description:");
    sPpd.description = wgtTextArea(root, PRJ_MAX_DESC);
    sPpd.description->weight = 100;
    sPpd.description->minH   = wgtPixels(PPD_DESC_H);
    wgtSetText(sPpd.description, prj->description);

    // OK / Cancel buttons
    WidgetT *btnRow = wgtHBox(root);
    btnRow->align = AlignCenterE;

    WidgetT *okBtn = wgtButton(btnRow, "&OK");
    okBtn->minW    = wgtPixels(PPD_BTN_W);
    okBtn->minH    = wgtPixels(PPD_BTN_H);
    okBtn->onClick = ppdOnOk;

    WidgetT *cancelBtn = wgtButton(btnRow, "&Cancel");
    cancelBtn->minW    = wgtPixels(PPD_BTN_W);
    cancelBtn->minH    = wgtPixels(PPD_BTN_H);
    cancelBtn->onClick = ppdOnCancel;

    dvxFitWindow(ctx, win);

    WindowT *prevModal = ctx->modalWindow;
    ctx->modalWindow   = win;

    while (!sPpd.done && ctx->running) {
        dvxUpdate(ctx);
    }

    if (sPpd.accepted) {
        const char *s;

        s = wgtGetText(sPpd.name);
        if (s) { snprintf(prj->name, sizeof(prj->name), "%s", s); }

        s = wgtGetText(sPpd.author);
        if (s) { snprintf(prj->author, sizeof(prj->author), "%s", s); }

        s = wgtGetText(sPpd.company);
        if (s) { snprintf(prj->company, sizeof(prj->company), "%s", s); }

        s = wgtGetText(sPpd.version);
        if (s) { snprintf(prj->version, sizeof(prj->version), "%s", s); }

        s = wgtGetText(sPpd.copyright);
        if (s) { snprintf(prj->copyright, sizeof(prj->copyright), "%s", s); }

        s = wgtGetText(sPpd.description);
        if (s) { snprintf(prj->description, sizeof(prj->description), "%s", s); }

        snprintf(prj->iconPath, sizeof(prj->iconPath), "%s", sPpd.iconPath);

        // Read startup form from dropdown
        if (sPpd.startupForm && sPpd.formNames) {
            int32_t sfIdx = wgtDropdownGetSelected(sPpd.startupForm);

            if (sfIdx >= 0 && sfIdx < (int32_t)arrlen(sPpd.formNames)) {
                snprintf(prj->startupForm, sizeof(prj->startupForm), "%s", sPpd.formNames[sfIdx]);
            }
        }

        prj->dirty = true;
    }

    // Free form name strings
    for (int32_t i = 0; i < (int32_t)arrlen(sPpd.formNames); i++) {
        free((char *)sPpd.formNames[i]);
    }
    arrfree(sPpd.formNames);
    sPpd.formNames = NULL;

    ctx->modalWindow = prevModal;
    dvxDestroyWindow(ctx, win);

    return sPpd.accepted;
}
