// formrt.c -- DVX BASIC form runtime implementation
//
// Bridges BASIC programs to the DVX widget system. All widget types
// are discovered dynamically via WgtIfaceT interface descriptors
// registered by .wgt DXE files. No hardcoded control types.

#include "formrt.h"
#include "../compiler/opcodes.h"
#include "../../../../libs/kpunch/serial/rs232/rs232.h"
#include "../../../../libs/kpunch/serial/seclink/secLink.h"
#include "dvxDlg.h"
#include "dvxRes.h"
#include "dvxWm.h"
#include "shellApp.h"
#include "box/box.h"
#include "ansiTerm/ansiTerm.h"
#include "dataCtrl/dataCtrl.h"
#include "dbGrid/dbGrid.h"
#include "thirdparty/stb_ds_wrap.h"

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

// ============================================================
// Defines
// ============================================================

#define DEFAULT_FORM_W       400
#define DEFAULT_FORM_H       300
#define MAX_EVENT_NAME_LEN   128

// Module-level form runtime pointer for onFormClose callback
static BasFormRtT   *sFormRt = NULL;


// ============================================================
// Module-scope declarations
// ============================================================

// Parse a filter string into FileFilterT entries.
//
// Format: "Label (pattern)|Label (pattern)|..."
// The pattern is extracted from inside the parentheses.
// Example: "Images (*.bmp;*.png;*.jpg)|Text (*.txt)|All Files (*.*)"
//
// If no parentheses, the entire entry is used as both label and pattern.
// If no pipe, treat as a single pattern for backward compatibility.

// No fixed file-filter cap: filters are heap-allocated per parse call.


typedef struct {
    int     (*open)(int, int32_t, int, char, int, int);
    int     (*close)(int);
    int     (*read)(int, char *, int);
    int     (*write)(int, const char *, int);
    int     (*writeBuf)(int, const char *, int);
    int     (*getRxBuffered)(int);
    int     (*clearRxBuffer)(int);
    int     (*getUartType)(int);
    int     (*getBase)(int);
    int     (*getIrq)(int);
    int     (*setBase)(int, int);
    int     (*setIrq)(int, int);
} SerApiT;

static SerApiT sSerApi;

static bool    sSerApiResolved = false;

// Per-COM raw serial state for terminal attach
#define SER_RECV_BUF    4096

typedef struct {
    bool     attached;
    WidgetT *term;
    int32_t  com;      // 0-based COM port index
} SerAttachT;

static SerAttachT sSerAttach[RS232_NUM_PORTS];


// SecLink function pointers (resolved lazily)
typedef struct {
    void *(*open)(int, int32_t, int, char, int, int, void (*)(void *, const uint8_t *, int, uint8_t), void *);
    void   (*close)(void *);
    int    (*handshake)(void *);
    bool   (*isReady)(void *);
    int    (*poll)(void *);
    int    (*send)(void *, const uint8_t *, int, uint8_t, bool, bool);
    int    (*sendBuf)(void *, const uint8_t *, int, uint8_t, bool);
    int    (*getPending)(void *);
} CommSecLinkApiT;

static CommSecLinkApiT sCommApi;

static bool            sCommApiResolved = false;

// Per-channel receive ring buffer
typedef struct {
    uint8_t  data[SECLINK_CHAN_BUF_SIZE];
    int32_t  head;
    int32_t  tail;
} CommChanBufT;

typedef struct {
    bool          active;
    void         *link;                          // SecLinkT*
    CommChanBufT  channels[SECLINK_NUM_CHANNELS];   // per-channel receive buffers
    WidgetT      *attachedTerm;                  // AnsiTerm widget if attached
    int32_t       termChannel;                   // channel for terminal I/O
    bool          termEncrypt;                    // encrypt terminal traffic
} CommSlotT;

// Dynamic array of heap-allocated comm slots (stb_ds).  Each slot is
// ~512KB (channel buffers), so we allocate lazily on CommOpen and free
// on CommClose.  Entries may be NULL where a slot has been closed --
// open scans for the first hole and reuses it, keeping handles stable.
static CommSlotT **sCommSlots = NULL;

// hlpcCompile function pointer (resolved lazily)
typedef int32_t (*HlpcCompileFnT)(const char **, int32_t, const char *, const char *, const char *, int32_t, void *, void *);

static HlpcCompileFnT sHlpcCompile = NULL;

static bool            sHlpcResolved = false;

// shellLoadAppWithArgs function pointer (resolved lazily)
typedef int32_t (*ShellLoadAppFnT)(void *, const char *, const char *);

static ShellLoadAppFnT sShellLoadApp = NULL;

static bool            sShellLoadResolved = false;

// Maximum number of simultaneously open resource handles
#define RES_MAX_HANDLES 8

static DvxResHandleT *sResHandles[RES_MAX_HANDLES];

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

int32_t basChoiceDialog(const char *title, const char *prompt, const char *items, int32_t defaultIdx);
BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType);
void *basExternResolve(void *ctx, const char *libName, const char *funcName);
const char *basFileOpen(const char *title, const char *filter);
const char *basFileSave(const char *title, const char *filter);
void basFormRtBindVm(BasFormRtT *rt);
BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc);
BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module);
WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout);
void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName);
void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef);
void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_t height);
WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox);
void basFormRtDestroy(BasFormRtT *rt);
void basFormRtEventLoop(BasFormRtT *rt);
void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName);
void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index);
bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName);
bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount);
static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName);
BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName);
void basFormRtHideForm(void *ctx, void *formRef);
static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText);
void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName);
static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataLen);
void *basFormRtLoadForm(void *ctx, const char *formName);
BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen);
int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title);
void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen);
void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen);
void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName);
void basFormRtRunSimple(BasFormRtT *rt);
void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName);
void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value);
void basFormRtShowForm(void *ctx, void *formRef, bool modal);
void basFormRtUnloadForm(void *ctx, void *formRef);
const char *basInputBox2(const char *title, const char *prompt, const char *defaultText);
int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal);
static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name);
int32_t basPromptSave(const char *title);
static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc);
void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt);
void CommClose(int32_t handle);
void CommDetach(int32_t handle);
static CommSlotT *commGetSlot(int32_t handle);
int32_t CommHandshake(int32_t handle);
static void commIdlePoll(void *ctx);
int32_t CommIsReady(int32_t handle);
static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel);
int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake);
int32_t CommPending(int32_t handle);
int32_t CommPoll(int32_t handle);
const char *CommRecv(int32_t handle, int32_t channel);
static bool commResolveApi(void);
int32_t CommSend(int32_t handle, const char *data, int32_t channel, int32_t encrypt);
static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen);
static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len);
WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent);
static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount);
static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled);
static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled);
int32_t HelpCompile(const char *inputFile, const char *outputFile);
void HelpView(const char *hlpFile);
static void onFormActivate(WindowT *win);
static void onFormClose(WindowT *win);
static void onFormDeactivate(WindowT *win);
static void onFormMenu(WindowT *win, int32_t menuId);
static void onFormResize(WindowT *win, int32_t newW, int32_t newH);
static void onWidgetBlur(WidgetT *w);
static void onWidgetChange(WidgetT *w);
static void onWidgetClick(WidgetT *w);
static void onWidgetDblClick(WidgetT *w);
static void onWidgetFocus(WidgetT *w);
static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift);
static void onWidgetKeyPress(WidgetT *w, int32_t keyAscii);
static void onWidgetKeyUp(WidgetT *w, int32_t keyCode, int32_t shift);
static void onWidgetMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onWidgetMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y);
static void onWidgetScroll(WidgetT *w, int32_t delta);
static bool onWidgetValidate(WidgetT *w);
static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, char *buf, int32_t bufSize);
static void parseFrmLine(const char *line, char *key, char *value);
static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl);
int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile);
int32_t ResAddText(const char *path, const char *name, const char *text);
void ResClose(int32_t handle);
int32_t ResCount(int32_t handle);
int32_t ResExtract(const char *path, const char *name, const char *outFile);
const char *ResGetText(const char *path, const char *name);
const char *ResName(int32_t handle, int32_t index);
static const char *resolveTypeName(const char *typeName);
int32_t ResOpen(const char *path);
int32_t ResRemove(const char *path, const char *name);
int32_t ResSize(int32_t handle, int32_t index);
int32_t ResType(int32_t handle, int32_t index);
void SerAttach(int32_t com, const char *termCtrlName);
int32_t SerAvailable(int32_t com);
void SerClose(int32_t com);
void SerDetach(int32_t com);
void SerFlush(int32_t com);
int32_t SerGetBase(int32_t com);
int32_t SerGetIrq(int32_t com);
int32_t SerGetUart(int32_t com);
static void serIdlePoll(void *ctx);
int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake);
const char *SerRead(int32_t com);
static bool serResolveApi(void);
void SerSetBase(int32_t com, int32_t base);
void SerSetIrq(int32_t com, int32_t irq);
static int32_t serTermRead(void *ctx, uint8_t *buf, int32_t maxLen);
static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len);
int32_t SerWrite(int32_t com, const char *data);
static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value);
static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value);
int32_t SQLAffected(int32_t db);
void SQLClose(int32_t db);
int32_t SQLEof(int32_t rs);
const char *SQLError(int32_t db);
int32_t SQLExec(int32_t db, const char *sql);
const char *SQLField(int32_t rs, const char *colName);
int32_t SQLFieldCount(int32_t rs);
double SQLFieldDbl(int32_t rs, int32_t col);
int32_t SQLFieldInt(int32_t rs, int32_t col);
const char *SQLFieldName(int32_t rs, int32_t col);
const char *SQLFieldText(int32_t rs, int32_t col);
void SQLFreeResult(int32_t rs);
int32_t SQLNext(int32_t rs);
int32_t SQLOpen(const char *path);
int32_t SQLQuery(int32_t db, const char *sql);
static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl);
static BasValueT zeroValue(void);

int32_t basChoiceDialog(const char *title, const char *prompt, const char *items, int32_t defaultIdx) {
    if (!sFormRt || !items) {
        return -1;
    }

    // Split pipe-delimited items string into an array
    static char itemBuf[1024];
    snprintf(itemBuf, sizeof(itemBuf), "%s", items);

    const char *ptrs[64];
    int32_t count = 0;
    char *tok     = itemBuf;

    while (*tok && count < 64) {
        ptrs[count++] = tok;
        char *sep = strchr(tok, '|');

        if (sep) {
            *sep = '\0';
            tok  = sep + 1;
        } else {
            break;
        }
    }

    if (count == 0) {
        return -1;
    }

    int32_t chosen = 0;

    if (dvxChoiceDialog(sFormRt->ctx, title, prompt, ptrs, count, defaultIdx, &chosen)) {
        return chosen;
    }

    return -1;
}


BasValueT basExternCall(void *ctx, void *funcPtr, const char *libName, const char *funcName, BasValueT *args, int32_t argc, uint8_t retType) {
    (void)ctx;
    (void)libName;
    (void)funcName;

    uint32_t nativeArgs[32];
    char    *tempStrings[16];
    int32_t  tempStringCount = 0;
    int32_t  nativeCount     = 0;

    for (int32_t i = 0; i < argc && i < 16; i++) {
        switch (args[i].type) {
            case BAS_TYPE_STRING: {
                BasStringT *s = basValFormatString(args[i]);
                char *cstr = strdup(s->data);
                basStringUnref(s);
                nativeArgs[nativeCount++] = (uint32_t)(uintptr_t)cstr;
                tempStrings[tempStringCount++] = cstr;
                break;
            }

            case BAS_TYPE_DOUBLE:
            case BAS_TYPE_SINGLE: {
                union { double d; uint32_t u[2]; } conv;
                conv.d = args[i].dblVal;
                nativeArgs[nativeCount++] = conv.u[0];
                nativeArgs[nativeCount++] = conv.u[1];
                break;
            }

            default: {
                nativeArgs[nativeCount++] = (uint32_t)(int32_t)basValToNumber(args[i]);
                break;
            }
        }
    }

    BasValueT result;
    memset(&result, 0, sizeof(result));

    if (retType == BAS_TYPE_DOUBLE || retType == BAS_TYPE_SINGLE) {
        double dblResult = 0.0;

        __asm__ __volatile__(
            "movl %%esp, %%ebx\n\t"
            ::: "ebx"
        );

        for (int32_t i = nativeCount - 1; i >= 0; i--) {
            uint32_t val = nativeArgs[i];
            __asm__ __volatile__("pushl %0" :: "r"(val));
        }

        __asm__ __volatile__(
            "call *%1\n\t"
            "fstpl %0\n\t"
            "movl %%ebx, %%esp\n\t"
            : "=m"(dblResult)
            : "r"(funcPtr)
            : "eax", "ecx", "edx", "memory"
        );

        result = basValDouble(dblResult);
    } else if (retType == BAS_TYPE_STRING) {
        uint32_t rawResult = 0;

        __asm__ __volatile__(
            "movl %%esp, %%ebx\n\t"
            ::: "ebx"
        );

        for (int32_t i = nativeCount - 1; i >= 0; i--) {
            uint32_t val = nativeArgs[i];
            __asm__ __volatile__("pushl %0" :: "r"(val));
        }

        __asm__ __volatile__(
            "call *%1\n\t"
            "movl %%ebx, %%esp\n\t"
            : "=a"(rawResult)
            : "r"(funcPtr)
            : "ecx", "edx", "memory"
        );

        const char *str = (const char *)(uintptr_t)rawResult;
        result = basValStringFromC(str ? str : "");
    } else {
        uint32_t rawResult = 0;

        __asm__ __volatile__(
            "movl %%esp, %%ebx\n\t"
            ::: "ebx"
        );

        for (int32_t i = nativeCount - 1; i >= 0; i--) {
            uint32_t val = nativeArgs[i];
            __asm__ __volatile__("pushl %0" :: "r"(val));
        }

        __asm__ __volatile__(
            "call *%1\n\t"
            "movl %%ebx, %%esp\n\t"
            : "=a"(rawResult)
            : "r"(funcPtr)
            : "ecx", "edx", "memory"
        );

        result = basValLong((int32_t)rawResult);
    }

    for (int32_t i = 0; i < tempStringCount; i++) {
        free(tempStrings[i]);
    }

    return result;
}


void *basExternResolve(void *ctx, const char *libName, const char *funcName) {
    (void)ctx;
    (void)libName;

    char mangledName[256];
    snprintf(mangledName, sizeof(mangledName), "_%s", funcName);

    return dlsym(NULL, mangledName);
}


const char *basFileOpen(const char *title, const char *filter) {
    if (!sFormRt) {
        return "";
    }

    FileFilterT *filters = NULL;
    char filterBuf[1024];
    int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf));

    static char path[DVX_MAX_PATH];
    path[0] = '\0';

    bool ok = dvxFileDialog(sFormRt->ctx, title, FD_OPEN, NULL, filters, count, path, sizeof(path));

    free(filters);

    if (ok) {
        return path;
    }

    return "";
}


const char *basFileSave(const char *title, const char *filter) {
    if (!sFormRt) {
        return "";
    }

    FileFilterT *filters = NULL;
    char filterBuf[1024];
    int32_t count = parseFileFilters(filter, &filters, filterBuf, sizeof(filterBuf));

    static char path[DVX_MAX_PATH];
    path[0] = '\0';

    bool ok = dvxFileDialog(sFormRt->ctx, title, FD_SAVE, NULL, filters, count, path, sizeof(path));

    free(filters);

    if (ok) {
        return path;
    }

    return "";
}


void basFormRtBindVm(BasFormRtT *rt) {
    BasUiCallbacksT ui;
    memset(&ui, 0, sizeof(ui));

    ui.getProp      = basFormRtGetProp;
    ui.setProp      = basFormRtSetProp;
    ui.callMethod   = basFormRtCallMethod;
    ui.createCtrl   = basFormRtCreateCtrl;
    ui.findCtrl     = basFormRtFindCtrl;
    ui.findCtrlIdx  = basFormRtFindCtrlIdx;
    ui.loadForm     = basFormRtLoadForm;
    ui.unloadForm   = basFormRtUnloadForm;
    ui.showForm     = basFormRtShowForm;
    ui.hideForm     = basFormRtHideForm;
    ui.msgBox       = basFormRtMsgBox;
    ui.inputBox     = basFormRtInputBox;
    ui.createForm   = basFormRtCreateForm;
    ui.createCtrlEx = basFormRtCreateCtrlEx;
    ui.setEvent     = basFormRtSetEvent;
    ui.removeCtrl   = basFormRtRemoveCtrl;
    ui.ctx          = rt;

    basVmSetUiCallbacks(rt->vm, &ui);
}


BasValueT basFormRtCallMethod(void *ctx, void *ctrlRef, const char *methodName, BasValueT *args, int32_t argc) {
    (void)ctx;
    BasControlT *ctrl = (BasControlT *)ctrlRef;

    if (!ctrl || !ctrl->widget) {
        return zeroValue();
    }

    // Interface descriptor methods
    const WgtIfaceT *iface = ctrl->iface;

    if (iface) {
        for (int32_t i = 0; i < iface->methodCount; i++) {
            if (strcasecmp(iface->methods[i].name, methodName) != 0) {
                continue;
            }

            const WgtMethodDescT *m = &iface->methods[i];
            WidgetT *w = ctrl->widget;

            switch (m->sig) {
                case WGT_SIG_VOID:
                    ((void (*)(WidgetT *))m->fn)(w);
                    return zeroValue();

                case WGT_SIG_INT:
                    if (argc >= 1) {
                        ((void (*)(WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]));
                    }
                    return zeroValue();

                case WGT_SIG_BOOL:
                    if (argc >= 1) {
                        ((void (*)(WidgetT *, bool))m->fn)(w, basValIsTruthy(args[0]));
                    }
                    return zeroValue();

                case WGT_SIG_STR:
                    if (argc >= 1) {
                        BasStringT *s = basValFormatString(args[0]);
                        ((void (*)(WidgetT *, const char *))m->fn)(w, s->data);
                        basStringUnref(s);
                    }
                    return zeroValue();

                case WGT_SIG_INT_INT:
                    if (argc >= 2) {
                        ((void (*)(WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]));
                    }
                    return zeroValue();

                case WGT_SIG_INT_BOOL:
                    if (argc >= 2) {
                        ((void (*)(WidgetT *, int32_t, bool))m->fn)(w, (int32_t)basValToNumber(args[0]), basValIsTruthy(args[1]));
                    }
                    return zeroValue();

                case WGT_SIG_RET_INT: {
                    int32_t v = ((int32_t (*)(const WidgetT *))m->fn)(w);
                    return basValLong(v);
                }

                case WGT_SIG_RET_BOOL: {
                    bool v = ((bool (*)(const WidgetT *))m->fn)(w);
                    return basValBool(v);
                }

                case WGT_SIG_RET_BOOL_INT: {
                    if (argc >= 1) {
                        bool v = ((bool (*)(const WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]));
                        return basValBool(v);
                    }
                    return basValBool(false);
                }

                case WGT_SIG_INT3:
                    if (argc >= 3) {
                        ((void (*)(WidgetT *, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2]));
                    }
                    return zeroValue();

                case WGT_SIG_INT4:
                    if (argc >= 4) {
                        ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2]), (int32_t)basValToNumber(args[3]));
                    }
                    return zeroValue();

                case WGT_SIG_RET_INT_INT_INT: {
                    if (argc >= 2) {
                        int32_t v = ((int32_t (*)(const WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]));
                        return basValLong(v);
                    }
                    return zeroValue();
                }

                case WGT_SIG_INT5:
                    if (argc >= 5) {
                        ((void (*)(WidgetT *, int32_t, int32_t, int32_t, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), (int32_t)basValToNumber(args[2]), (int32_t)basValToNumber(args[3]), (int32_t)basValToNumber(args[4]));
                    }
                    return zeroValue();

                case WGT_SIG_RET_STR_INT: {
                    if (argc >= 1) {
                        const char *s = ((const char *(*)(const WidgetT *, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]));
                        return basValStringFromC(s ? s : "");
                    }
                    return basValStringFromC("");
                }

                case WGT_SIG_RET_STR_INT_INT: {
                    if (argc >= 2) {
                        const char *s = ((const char *(*)(const WidgetT *, int32_t, int32_t))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]));
                        return basValStringFromC(s ? s : "");
                    }
                    return basValStringFromC("");
                }

                case WGT_SIG_INT_INT_STR:
                    if (argc >= 3) {
                        BasStringT *s = basValFormatString(args[2]);
                        ((void (*)(WidgetT *, int32_t, int32_t, const char *))m->fn)(w, (int32_t)basValToNumber(args[0]), (int32_t)basValToNumber(args[1]), s->data);
                        basStringUnref(s);
                    }
                    return zeroValue();

                case WGT_SIG_STR_BOOL_BOOL: {
                    if (argc >= 3) {
                        BasStringT *s = basValFormatString(args[0]);
                        bool v = ((bool (*)(WidgetT *, const char *, bool, bool))m->fn)(w, s->data, basValIsTruthy(args[1]), basValIsTruthy(args[2]));
                        basStringUnref(s);
                        return basValBool(v);
                    }
                    return basValBool(false);
                }

                case WGT_SIG_STR_STR_BOOL: {
                    if (argc >= 3) {
                        BasStringT *s1 = basValFormatString(args[0]);
                        BasStringT *s2 = basValFormatString(args[1]);
                        int32_t v = ((int32_t (*)(WidgetT *, const char *, const char *, bool))m->fn)(w, s1->data, s2->data, basValIsTruthy(args[2]));
                        basStringUnref(s1);
                        basStringUnref(s2);
                        return basValLong(v);
                    }
                    return zeroValue();
                }

                case WGT_SIG_RET_STR: {
                    const char *s = ((const char *(*)(const WidgetT *))m->fn)(w);
                    return basValStringFromC(s ? s : "");
                }

                case WGT_SIG_STR_INT:
                    if (argc >= 2) {
                        BasStringT *s = basValFormatString(args[0]);
                        ((void (*)(WidgetT *, const char *, int32_t))m->fn)(w, s->data, (int32_t)basValToNumber(args[1]));
                        basStringUnref(s);
                    }
                    return zeroValue();

                case WGT_SIG_INT_STR:
                    if (argc >= 2) {
                        BasStringT *s = basValFormatString(args[1]);
                        ((void (*)(WidgetT *, int32_t, const char *))m->fn)(w, (int32_t)basValToNumber(args[0]), s->data);
                        basStringUnref(s);
                    }
                    return zeroValue();
            }
        }
    }

    // Common methods (SetFocus, Refresh)
    return callCommonMethod(ctrl, methodName, args, argc);
}


BasFormRtT *basFormRtCreate(AppContextT *ctx, BasVmT *vm, BasModuleT *module) {
    BasFormRtT *rt = (BasFormRtT *)calloc(1, sizeof(BasFormRtT));

    if (!rt) {
        return NULL;
    }

    rt->ctx    = ctx;
    rt->vm     = vm;
    rt->module = module;

    sFormRt = rt;
    basFormRtBindVm(rt);
    return rt;
}


WidgetT *basFormRtCreateContentBox(WidgetT *root, const char *layout) {
    if (!layout || !layout[0] || strcasecmp(layout, "VBox") == 0) {
        return root;
    }

    const char *wgtName = wgtFindByBasName(layout);

    if (wgtName) {
        const WgtIfaceT *iface = wgtGetIface(wgtName);

        if (iface && iface->isContainer && iface->createSig == WGT_CREATE_PARENT) {
            const void *api = wgtGetApi(wgtName);

            if (api) {
                WidgetT *(*createFn)(WidgetT *) = *(WidgetT *(*const *)(WidgetT *))api;
                WidgetT *box = createFn(root);
                box->weight = 100;
                return box;
            }
        }
    }

    return root;
}


void *basFormRtCreateCtrl(void *ctx, void *formRef, const char *typeName, const char *ctrlName) {
    (void)ctx;
    BasFormT *form = (BasFormT *)formRef;

    if (!form) {
        return NULL;
    }

    // Resolve VB name to DVX widget type name
    const char *wgtTypeName = resolveTypeName(typeName);

    if (!wgtTypeName) {
        return NULL;
    }

    // Create the widget
    WidgetT *parent = form->contentBox ? form->contentBox : form->root;

    if (!parent) {
        return NULL;
    }

    WidgetT *widget = createWidget(wgtTypeName, parent);

    if (!widget) {
        return NULL;
    }

    wgtSetName(widget, ctrlName);

    // Initialize control entry
    BasControlT *ctrl = (BasControlT *)calloc(1, sizeof(BasControlT));

    if (!ctrl) {
        return NULL;
    }

    ctrl->index  = -1;
    snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
    ctrl->widget = widget;
    ctrl->form   = form;
    ctrl->iface  = wgtGetIface(wgtTypeName);
    arrput(form->controls, ctrl);


    // Wire up event callbacks
    widget->userData   = ctrl;
    widget->onClick    = onWidgetClick;
    widget->onDblClick = onWidgetDblClick;
    widget->onChange    = onWidgetChange;
    widget->onFocus    = onWidgetFocus;
    widget->onBlur     = onWidgetBlur;
    widget->onValidate = onWidgetValidate;

    return ctrl;
}


void *basFormRtCreateCtrlEx(void *ctx, void *formRef, const char *typeName, const char *ctrlName, void *parentRef) {
    (void)ctx;
    BasFormT *form = (BasFormT *)formRef;

    if (!form) {
        return NULL;
    }

    const char *wgtTypeName = resolveTypeName(typeName);

    if (!wgtTypeName) {
        return NULL;
    }

    // Determine parent widget: if parentRef is a control, use its widget;
    // otherwise fall back to the form's content box.
    WidgetT *parent = NULL;

    if (parentRef) {
        BasControlT *parentCtrl = (BasControlT *)parentRef;
        parent = parentCtrl->widget;
    }

    if (!parent) {
        parent = form->contentBox ? form->contentBox : form->root;
    }

    if (!parent) {
        return NULL;
    }

    WidgetT *widget = createWidget(wgtTypeName, parent);

    if (!widget) {
        return NULL;
    }

    wgtSetName(widget, ctrlName);

    BasControlT *ctrl = (BasControlT *)calloc(1, sizeof(BasControlT));

    if (!ctrl) {
        return NULL;
    }

    ctrl->index  = -1;
    snprintf(ctrl->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
    ctrl->widget = widget;
    ctrl->form   = form;
    ctrl->iface  = wgtGetIface(wgtTypeName);
    arrput(form->controls, ctrl);


    widget->userData   = ctrl;
    widget->onClick    = onWidgetClick;
    widget->onDblClick = onWidgetDblClick;
    widget->onChange    = onWidgetChange;
    widget->onFocus    = onWidgetFocus;
    widget->onBlur     = onWidgetBlur;
    widget->onValidate = onWidgetValidate;

    return ctrl;
}


void *basFormRtCreateForm(void *ctx, const char *formName, int32_t width, int32_t height) {
    BasFormRtT *rt = (BasFormRtT *)ctx;

    if (!rt || !formName) {
        return NULL;
    }

    // Check if form already exists
    for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
        if (strcasecmp(rt->forms[i]->name, formName) == 0) {
            return rt->forms[i];
        }
    }

    if (width <= 0) {
        width = DEFAULT_FORM_W;
    }

    if (height <= 0) {
        height = DEFAULT_FORM_H;
    }

    WidgetT *root;
    WidgetT *contentBox;
    WindowT *win = basFormRtCreateFormWindow(rt->ctx, formName, "VBox", true, true, false, width, height, 0, 0, &root, &contentBox);

    if (!win) {
        return NULL;
    }

    BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT));
    arrput(rt->forms, form);

    snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName);
    snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox");
    form->frmWidth     = width;
    form->frmHeight    = height;
    form->frmResizable = true;
    form->frmCentered  = true;
    win->onClose       = onFormClose;
    win->onResize      = onFormResize;
    win->onFocus       = onFormActivate;
    win->onBlur        = onFormDeactivate;
    form->window       = win;
    form->root         = root;
    form->contentBox   = contentBox;
    form->ctx          = rt->ctx;
    form->vm           = rt->vm;
    form->module       = rt->module;

    // Synthetic control for form-level property access
    memset(&form->formCtrl, 0, sizeof(form->formCtrl));
    snprintf(form->formCtrl.name, BAS_MAX_CTRL_NAME, "%s", formName);
    form->formCtrl.widget = root;
    form->formCtrl.form   = form;

    // Allocate per-form variable storage from module metadata
    if (rt->module && rt->module->formVarInfo) {
        for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) {
            if (strcasecmp(rt->module->formVarInfo[j].formName, formName) == 0) {
                int32_t vc = rt->module->formVarInfo[j].varCount;

                if (vc > 0) {
                    form->formVars     = (BasValueT *)calloc(vc, sizeof(BasValueT));
                    form->formVarCount = vc;
                }

                int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr;

                if (initAddr >= 0 && rt->vm) {
                    basVmSetCurrentForm(rt->vm, form);
                    basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
                    basVmCallSub(rt->vm, initAddr);
                    basVmSetCurrentForm(rt->vm, NULL);
                    basVmSetCurrentFormVars(rt->vm, NULL, 0);
                }

                break;
            }
        }
    }

    return form;
}


WindowT *basFormRtCreateFormWindow(AppContextT *ctx, const char *title, const char *layout, bool resizable, bool centered, bool autoSize, int32_t width, int32_t height, int32_t left, int32_t top, WidgetT **outRoot, WidgetT **outContentBox) {
    int32_t defW = (width > 0)  ? width  : DEFAULT_FORM_W;
    int32_t defH = (height > 0) ? height : DEFAULT_FORM_H;

    WindowT *win = dvxCreateWindowCentered(ctx, title, defW, defH, resizable);

    if (!win) {
        return NULL;
    }

    win->visible = false;

    WidgetT *root = wgtInitWindow(ctx, win);

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

    WidgetT *contentBox = basFormRtCreateContentBox(root, layout);

    if (autoSize) {
        dvxFitWindow(ctx, win);
    } else if (width > 0 && height > 0) {
        dvxResizeWindow(ctx, win, width, height);
    }

    if (centered) {
        win->x = (ctx->display.width - win->w) / 2;
        win->y = (ctx->display.height - win->h) / 2;
    } else if (left > 0 || top > 0) {
        win->x = left;
        win->y = top;
    }

    *outRoot       = root;
    *outContentBox = contentBox;
    return win;
}


void basFormRtDestroy(BasFormRtT *rt) {
    if (!rt) {
        return;
    }

    if (sFormRt == rt) {
        sFormRt = NULL;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
        BasFormT *form = rt->forms[i];

        if (form->formVars) {
            for (int32_t j = 0; j < form->formVarCount; j++) {
                basValRelease(&form->formVars[j]);
            }

            free(form->formVars);
        }

        for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) {
            free(form->controls[j]);
        }

        arrfree(form->controls);

        for (int32_t j = 0; j < form->menuIdMapCount; j++) {
            free(form->menuIdMap[j].proxy);
        }

        arrfree(form->menuIdMap);

        if (form->window) {
            dvxDestroyWindow(rt->ctx, form->window);
            form->window = NULL;
        }

        free(form);
    }

    arrfree(rt->forms);

    // Free .frm source cache
    for (int32_t i = 0; i < rt->frmCacheCount; i++) {
        free(rt->frmCache[i].frmSource);
    }

    arrfree(rt->frmCache);
    free(rt);
}


void basFormRtEventLoop(BasFormRtT *rt) {
    if (!rt || !rt->ctx || !rt->vm) {
        return;
    }

    while (rt->ctx->running && arrlen(rt->forms) > 0) {
        if (!dvxUpdate(rt->ctx)) {
            break;
        }
    }
}


void *basFormRtFindCtrl(void *ctx, void *formRef, const char *ctrlName) {
    BasFormRtT *rt = (BasFormRtT *)ctx;
    BasFormT *form = (BasFormT *)formRef;

    if (!form) {
        return NULL;
    }

    // Check if the name refers to the current form itself
    if (strcasecmp(form->name, ctrlName) == 0) {
        return &form->formCtrl;
    }

    // Search controls on the current form
    for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
        if (strcasecmp(form->controls[i]->name, ctrlName) == 0) {
            return form->controls[i];
        }
    }

    // Search menu items on the current form
    for (int32_t i = 0; i < form->menuIdMapCount; i++) {
        if (strcasecmp(form->menuIdMap[i].name, ctrlName) == 0) {
            // Create proxy on first access
            if (!form->menuIdMap[i].proxy) {
                BasControlT *proxy = (BasControlT *)calloc(1, sizeof(BasControlT));
                snprintf(proxy->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
                proxy->form   = form;
                proxy->menuId = form->menuIdMap[i].id;
                form->menuIdMap[i].proxy = proxy;
            }
            return form->menuIdMap[i].proxy;
        }
    }

    // Search other loaded forms by name (for cross-form property access)
    if (rt) {
        for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
            if (strcasecmp(rt->forms[i]->name, ctrlName) == 0) {
                return &rt->forms[i]->formCtrl;
            }
        }
    }

    return NULL;
}


void *basFormRtFindCtrlIdx(void *ctx, void *formRef, const char *ctrlName, int32_t index) {
    (void)ctx;
    BasFormT *form = (BasFormT *)formRef;

    if (!form) {
        return NULL;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
        if (form->controls[i]->index == index && strcasecmp(form->controls[i]->name, ctrlName) == 0) {
            return form->controls[i];
        }
    }

    return NULL;
}


bool basFormRtFireEvent(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) {
    return basFormRtFireEventArgs(rt, form, ctrlName, eventName, NULL, 0);
}


bool basFormRtFireEventArgs(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName, const BasValueT *args, int32_t argCount) {
    if (!rt || !form || !rt->vm || !rt->module) {
        return false;
    }

    char handlerName[MAX_EVENT_NAME_LEN];
    snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName);

    const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);

    if (!proc) {
        return false;
    }

    if (proc->isFunction) {
        return false;
    }

    // Strict parameter matching: the sub must declare exactly the
    // number of parameters the event provides, or zero (no params).
    if (proc->paramCount != 0 && proc->paramCount != argCount) {
        return false;
    }

    BasFormT  *prevForm     = rt->currentForm;
    BasValueT *prevVars     = rt->vm->currentFormVars;
    int32_t    prevVarCount = rt->vm->currentFormVarCount;

    rt->currentForm = form;
    basVmSetCurrentForm(rt->vm, form);
    basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);

    bool ok;

    if (argCount > 0 && args) {
        ok = basVmCallSubWithArgs(rt->vm, proc->codeAddr, args, argCount);
    } else {
        ok = basVmCallSub(rt->vm, proc->codeAddr);
    }

    rt->currentForm = prevForm;
    basVmSetCurrentForm(rt->vm, prevForm);
    basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
    return ok;
}


// ============================================================
// basFormRtFireEventWithCancel -- fire an event that has a Cancel
// parameter (first arg, Integer). Returns true if Cancel was set
// to non-zero by the event handler.

static bool basFormRtFireEventWithCancel(BasFormRtT *rt, BasFormT *form, const char *ctrlName, const char *eventName) {
    if (!rt || !form || !rt->vm || !rt->module) {
        return false;
    }

    char handlerName[MAX_EVENT_NAME_LEN];
    snprintf(handlerName, sizeof(handlerName), "%s_%s", ctrlName, eventName);

    const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);

    if (!proc || proc->isFunction) {
        return false;
    }

    // Must accept 0 or 1 parameter
    if (proc->paramCount != 0 && proc->paramCount != 1) {
        return false;
    }

    BasFormT  *prevForm     = rt->currentForm;
    BasValueT *prevVars     = rt->vm->currentFormVars;
    int32_t    prevVarCount = rt->vm->currentFormVarCount;

    rt->currentForm = form;
    basVmSetCurrentForm(rt->vm, form);
    basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);

    bool cancelled = false;

    if (proc->paramCount == 1) {
        BasValueT args[1];
        args[0] = basValLong(0); // Cancel = 0 (don't cancel)

        BasValueT outArgs[1];
        memset(outArgs, 0, sizeof(outArgs));

        if (basVmCallSubWithArgsOut(rt->vm, proc->codeAddr, args, 1, outArgs, 1)) {
            cancelled = (basValToNumber(outArgs[0]) != 0);
            basValRelease(&outArgs[0]);
        }
    } else {
        basVmCallSub(rt->vm, proc->codeAddr);
    }

    rt->currentForm = prevForm;
    basVmSetCurrentForm(rt->vm, prevForm);
    basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
    return cancelled;
}


BasValueT basFormRtGetProp(void *ctx, void *ctrlRef, const char *propName) {
    (void)ctx;
    BasControlT *ctrl = (BasControlT *)ctrlRef;

    if (!ctrl) {
        return zeroValue();
    }

    // Menu item properties
    if (ctrl->menuId > 0 && ctrl->form && ctrl->form->window && ctrl->form->window->menuBar) {
        MenuBarT *bar = ctrl->form->window->menuBar;

        if (strcasecmp(propName, "Checked") == 0) {
            return basValBool(wmMenuItemIsChecked(bar, ctrl->menuId));
        }

        if (strcasecmp(propName, "Enabled") == 0) {
            return basValBool(true); // TODO: wmMenuItemIsEnabled not exposed yet
        }

        if (strcasecmp(propName, "Name") == 0) {
            return basValStringFromC(ctrl->name);
        }

        return zeroValue();
    }

    if (!ctrl->widget) {
        return zeroValue();
    }

    // Form-level properties use the window and BasFormT, not the root widget
    if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
        WindowT *win  = ctrl->form->window;
        BasFormT *frm = ctrl->form;

        if (strcasecmp(propName, "Name") == 0)      { return basValStringFromC(frm->name); }
        if (strcasecmp(propName, "Caption") == 0)    { return basValStringFromC(win ? win->title : ""); }
        if (strcasecmp(propName, "Width") == 0)      { return basValLong(win ? win->w : 0); }
        if (strcasecmp(propName, "Height") == 0)     { return basValLong(win ? win->h : 0); }
        if (strcasecmp(propName, "Left") == 0)       { return basValLong(win ? win->x : 0); }
        if (strcasecmp(propName, "Top") == 0)        { return basValLong(win ? win->y : 0); }
        if (strcasecmp(propName, "Visible") == 0)    { return basValBool(win && win->visible); }
        if (strcasecmp(propName, "Resizable") == 0)  { return basValBool(win && win->resizable); }
        if (strcasecmp(propName, "AutoSize") == 0)   { return basValBool(frm->frmAutoSize); }
        if (strcasecmp(propName, "Centered") == 0)   { return basValBool(frm->frmCentered); }
        if (strcasecmp(propName, "Layout") == 0)     { return basValStringFromC(frm->frmLayout); }

        return zeroValue();
    }

    // Common properties (Name, Left, Top, Width, Height, Visible, Enabled)
    bool handled;
    BasValueT val = getCommonProp(ctrl, propName, &handled);

    if (handled) {
        return val;
    }

    // "Caption" and "Text" map to wgtGetText for all widgets
    if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
        const char *text = wgtGetText(ctrl->widget);
        return basValStringFromC(text ? text : "");
    }

    // Help topic
    if (strcasecmp(propName, "HelpTopic") == 0) {
        return basValStringFromC(ctrl->helpTopic);
    }

    // Data binding properties
    if (strcasecmp(propName, "DataSource") == 0) {
        return basValStringFromC(ctrl->dataSource);
    }

    if (strcasecmp(propName, "DataField") == 0) {
        return basValStringFromC(ctrl->dataField);
    }

    // "ListCount" for any widget with item storage
    if (strcasecmp(propName, "ListCount") == 0) {
        if (ctrl->iface) {
            for (int32_t m = 0; m < ctrl->iface->methodCount; m++) {
                if (strcasecmp(ctrl->iface->methods[m].name, "ListCount") == 0) {
                    return basValLong(((int32_t (*)(const WidgetT *))ctrl->iface->methods[m].fn)(ctrl->widget));
                }
            }
        }

        return basValLong(0);
    }

    // Interface descriptor properties
    if (ctrl->iface) {
        val = getIfaceProp(ctrl->iface, ctrl->widget, propName, &handled);

        if (handled) {
            return val;
        }
    }

    return zeroValue();
}


void basFormRtHideForm(void *ctx, void *formRef) {
    BasFormRtT *rt   = (BasFormRtT *)ctx;
    BasFormT   *form = (BasFormT *)formRef;

    if (!form || !form->window) {
        return;
    }

    dvxHideWindow(rt->ctx, form->window);
}


static BasStringT *basFormRtInputBox(void *ctx, const char *prompt, const char *title, const char *defaultText) {
    BasFormRtT *rt = (BasFormRtT *)ctx;
    char buf[256];

    buf[0] = '\0';

    if (defaultText && defaultText[0]) {
        strncpy(buf, defaultText, sizeof(buf) - 1);
        buf[sizeof(buf) - 1] = '\0';
    }

    if (!dvxInputBox(rt->ctx, title, prompt, defaultText, buf, sizeof(buf))) {
        return NULL;
    }

    return basStringNew(buf, (int32_t)strlen(buf));
}


void basFormRtLoadAllForms(BasFormRtT *rt, const char *startupFormName) {
    if (!rt) {
        return;
    }

    // Load .frm text cache entries (skip if already loaded)
    for (int32_t i = 0; i < rt->frmCacheCount; i++) {
        basFormRtLoadForm(rt, rt->frmCache[i].formName);
    }

    // Load compiled form cache entries (skip if already loaded)
    for (int32_t i = 0; i < rt->cfmCacheCount; i++) {
        basFormRtLoadForm(rt, rt->cfmCache[i].formName);
    }

    // Show the startup form (named or first)
    int32_t formCount = (int32_t)arrlen(rt->forms);

    if (formCount > 0) {
        BasFormT *startup = rt->forms[0];

        if (startupFormName && startupFormName[0]) {
            for (int32_t i = 0; i < formCount; i++) {
                if (strcasecmp(rt->forms[i]->name, startupFormName) == 0) {
                    startup = rt->forms[i];
                    break;
                }
            }
        }

        basFormRtShowForm(rt, startup, false);
    }
}


static void *basFormRtLoadCfm(BasFormRtT *rt, const uint8_t *data, int32_t dataLen) {
    (void)rt; (void)data; (void)dataLen;
    return NULL;
    // Unreachable past here; left to satisfy the rest of the function body
    // which assumed a live formcfm implementation.
    BasFormT *form = NULL;

    if (!form || !form->window) {
        return form;
    }

    dvxLog("basFormRtLoadCfm: form '%s' created, %ld controls", form->name, (long)arrlen(form->controls));

    // Set window callbacks (same as basFormRtLoadForm)
    form->window->onClose  = onFormClose;
    form->window->onResize = onFormResize;
    form->window->onFocus  = onFormActivate;
    form->window->onBlur   = onFormDeactivate;

    // Wire event callbacks on all controls
    for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
        BasControlT *ctrl = form->controls[i];

        if (ctrl->widget) {
            ctrl->widget->onClick    = onWidgetClick;
            ctrl->widget->onDblClick = onWidgetDblClick;
            ctrl->widget->onChange   = onWidgetChange;
            ctrl->widget->onFocus    = onWidgetFocus;
            ctrl->widget->onBlur     = onWidgetBlur;
            ctrl->widget->onValidate = onWidgetValidate;
            ctrl->widget->onKeyPress = onWidgetKeyPress;
            ctrl->widget->onKeyDown  = onWidgetKeyDown;
            ctrl->widget->onKeyUp    = onWidgetKeyUp;
            ctrl->widget->onMouseDown = onWidgetMouseDown;
            ctrl->widget->onMouseUp   = onWidgetMouseUp;
            ctrl->widget->onMouseMove = onWidgetMouseMove;
            ctrl->widget->onScroll    = onWidgetScroll;
        }
    }

    // Allocate per-form variable storage
    if (rt->module && rt->module->formVarInfo) {
        for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) {
            if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) == 0) {
                int32_t vc = rt->module->formVarInfo[j].varCount;

                if (vc > 0) {
                    form->formVars     = (BasValueT *)calloc(vc, sizeof(BasValueT));
                    form->formVarCount = vc;
                }

                int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr;

                if (initAddr >= 0 && rt->vm) {
                    basVmSetCurrentForm(rt->vm, form);
                    basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
                    basVmCallSub(rt->vm, initAddr);
                    basVmSetCurrentForm(rt->vm, NULL);
                    basVmSetCurrentFormVars(rt->vm, NULL, 0);
                }

                break;
            }
        }
    }

    // Fire Form_Load event
    basFormRtFireEvent(rt, form, form->name, "Load");

    return form;
}


void *basFormRtLoadForm(void *ctx, const char *formName) {
    BasFormRtT *rt = (BasFormRtT *)ctx;

    // Check if form already exists
    for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
        if (strcasecmp(rt->forms[i]->name, formName) == 0) {
            return rt->forms[i];
        }
    }

    // Check the .frm cache for reload after unload.
    // Use a static guard to prevent recursion: basFormRtLoadFrm calls
    // basFormRtLoadForm for the "Begin Form" line, which would re-enter
    // this function. The guard lets the recursive call fall through to
    // bare form creation, which basFormRtLoadFrm then populates.
    static bool sLoadingFrm = false;

    if (!sLoadingFrm) {
        for (int32_t i = 0; i < rt->frmCacheCount; i++) {
            if (strcasecmp(rt->frmCache[i].formName, formName) == 0) {
                char   *src    = rt->frmCache[i].frmSource;
                int32_t srcLen = rt->frmCache[i].frmSourceLen;
                arrdel(rt->frmCache, i);
                rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);

                sLoadingFrm = true;
                BasFormT *form = basFormRtLoadFrm(rt, src, srcLen);
                sLoadingFrm = false;
                free(src);
                return form;
            }
        }
    }

    // Check the compiled form cache (standalone apps)
    for (int32_t i = 0; i < rt->cfmCacheCount; i++) {
        if (strcasecmp(rt->cfmCache[i].formName, formName) == 0) {
            return basFormRtLoadCfm(rt, rt->cfmCache[i].data, rt->cfmCache[i].dataLen);
        }
    }

    // No cache entry — create a bare form (first-time load without .frm file)
    WidgetT *root;
    WidgetT *bareContentBox;
    WindowT *win = basFormRtCreateFormWindow(rt->ctx, formName, "VBox", false, true, false, DEFAULT_FORM_W, DEFAULT_FORM_H, 0, 0, &root, &bareContentBox);

    if (!win) {
        return NULL;
    }

    BasFormT *form = (BasFormT *)calloc(1, sizeof(BasFormT));
    arrput(rt->forms, form);

    snprintf(form->name, BAS_MAX_CTRL_NAME, "%s", formName);
    snprintf(form->frmLayout, sizeof(form->frmLayout), "VBox");
    win->onClose     = onFormClose;
    win->onResize    = onFormResize;
    win->onFocus     = onFormActivate;
    win->onBlur      = onFormDeactivate;
    form->window     = win;
    form->root       = root;
    form->contentBox = bareContentBox;
    form->ctx        = rt->ctx;
    form->vm         = rt->vm;
    form->module     = rt->module;

    // Initialize synthetic control for form-level property access
    memset(&form->formCtrl, 0, sizeof(form->formCtrl));
    snprintf(form->formCtrl.name, BAS_MAX_CTRL_NAME, "%s", formName);
    form->formCtrl.widget = root;
    form->formCtrl.form   = form;

    // Allocate per-form variable storage and run init code from module metadata
    if (rt->module && rt->module->formVarInfo) {
        for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) {
            if (strcasecmp(rt->module->formVarInfo[j].formName, formName) == 0) {
                int32_t vc = rt->module->formVarInfo[j].varCount;

                if (vc > 0) {
                    form->formVars     = (BasValueT *)calloc(vc, sizeof(BasValueT));
                    form->formVarCount = vc;
                }

                // Execute per-form init block (DIM arrays, UDT init)
                int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr;

                if (initAddr >= 0 && rt->vm) {
                    basVmSetCurrentForm(rt->vm, form);
                    basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
                    basVmCallSub(rt->vm, initAddr);
                    basVmSetCurrentForm(rt->vm, NULL);
                    basVmSetCurrentFormVars(rt->vm, NULL, 0);
                }

                break;
            }
        }
    }

    return form;
}


BasFormT *basFormRtLoadFrm(BasFormRtT *rt, const char *source, int32_t sourceLen) {
    if (!rt || !source || sourceLen <= 0) {
        return NULL;
    }

    BasFormT    *form    = NULL;
    BasControlT *current = NULL;

    WidgetT *parentStack[BAS_MAX_FRM_NESTING];
    int32_t  nestDepth = 0;

    // Track Begin/End blocks: true = container (Form/Frame), false = control
    bool    isContainer[BAS_MAX_FRM_NESTING];
    int32_t blockDepth = 0;

    // Temporary menu item accumulation
    typedef struct {
        char    caption[256];
        char    name[BAS_MAX_CTRL_NAME];
        int32_t level;
        bool    checked;
        bool    radioCheck;
        bool    enabled;
    } TempMenuItemT;

    TempMenuItemT *menuItems    = NULL; // stb_ds array
    TempMenuItemT *curMenuItem  = NULL;
    int32_t        menuNestDepth = 0;
    bool           inMenu        = false;

    const char *pos = source;
    const char *end = source + sourceLen;

    while (pos < end) {
        const char *lineStart = pos;

        while (pos < end && *pos != '\n' && *pos != '\r') {
            pos++;
        }

        int32_t lineLen = (int32_t)(pos - lineStart);

        if (pos < end && *pos == '\r') {
            pos++;
        }

        if (pos < end && *pos == '\n') {
            pos++;
        }

        char line[BAS_MAX_FRM_LINE_LEN];

        if (lineLen >= BAS_MAX_FRM_LINE_LEN) {
            lineLen = BAS_MAX_FRM_LINE_LEN - 1;
        }

        memcpy(line, lineStart, lineLen);
        line[lineLen] = '\0';

        const char *trimmed = dvxSkipWs(line);

        if (*trimmed == '\0' || *trimmed == '\'') {
            continue;
        }

        // "VERSION DVX x.xx" (native) or "VERSION x.xx" (VB import)
        if (strncasecmp(trimmed, "VERSION ", 8) == 0) {
            const char *ver = trimmed + 8;

            if (strncasecmp(ver, "DVX ", 4) != 0) {
                double vbVer = atof(ver);

                if (vbVer > 2.0) {
                    return NULL; // VB4+ form, not compatible
                }
            }

            continue;
        }

        // "Begin TypeName CtrlName"
        if (strncasecmp(trimmed, "Begin ", 6) == 0) {
            const char *rest = trimmed + 6;
            char  typeName[BAS_MAX_CTRL_NAME];
            char  ctrlName[BAS_MAX_CTRL_NAME];

            int32_t ti = 0;

            while (*rest && *rest != ' ' && *rest != '\t' && ti < BAS_MAX_CTRL_NAME - 1) {
                typeName[ti++] = *rest++;
            }

            typeName[ti] = '\0';

            rest = dvxSkipWs(rest);

            int32_t ci = 0;

            while (*rest && *rest != ' ' && *rest != '\t' && *rest != '\r' && *rest != '\n' && ci < BAS_MAX_CTRL_NAME - 1) {
                ctrlName[ci++] = *rest++;
            }

            ctrlName[ci] = '\0';

            if (strcasecmp(typeName, "Form") == 0) {
                form = (BasFormT *)basFormRtLoadForm(rt, ctrlName);

                if (!form) {
                    return NULL;
                }

                // contentBox may already be set from basFormRtLoadForm.
                // It gets replaced at the End block after Layout is known.
                nestDepth = 1;
                parentStack[0] = form->contentBox;
                current   = NULL;

                if (blockDepth < BAS_MAX_FRM_NESTING) {
                    isContainer[blockDepth++] = true;
                }
            } else if (strcasecmp(typeName, "Menu") == 0 && form) {
                TempMenuItemT mi;
                memset(&mi, 0, sizeof(mi));
                snprintf(mi.name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
                mi.level   = menuNestDepth;
                mi.enabled = true;
                arrput(menuItems, mi);
                curMenuItem = &menuItems[arrlen(menuItems) - 1];
                current     = NULL;
                menuNestDepth++;
                inMenu = true;

                if (blockDepth < BAS_MAX_FRM_NESTING) {
                    isContainer[blockDepth++] = false;
                }

                continue;
            } else if (form && nestDepth > 0) {
                // Create the content box on first control if not yet done
                if (!form->contentBox && form->root) {
                    form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout);
                    parentStack[0] = form->contentBox;
                }
                WidgetT *parent = parentStack[nestDepth - 1];

                const char *wgtTypeName = resolveTypeName(typeName);

                if (!wgtTypeName) {
                    continue;
                }

                WidgetT *widget = createWidget(wgtTypeName, parent);

                if (!widget) {
                    continue;
                }

                wgtSetName(widget, ctrlName);

                {
                    BasControlT *ctrlEntry = (BasControlT *)calloc(1, sizeof(BasControlT));

                    if (!ctrlEntry) {
                        continue;
                    }

                    snprintf(ctrlEntry->name, BAS_MAX_CTRL_NAME, "%s", ctrlName);
                    snprintf(ctrlEntry->typeName, BAS_MAX_CTRL_NAME, "%s", typeName);
                    ctrlEntry->index  = -1;
                    ctrlEntry->widget = widget;
                    ctrlEntry->form   = form;
                    ctrlEntry->iface  = wgtGetIface(wgtTypeName);
                    arrput(form->controls, ctrlEntry);
                

                    current = ctrlEntry;
                    widget->userData   = current;
                    widget->onClick     = onWidgetClick;
                    widget->onDblClick  = onWidgetDblClick;
                    widget->onChange     = onWidgetChange;
                    widget->onFocus     = onWidgetFocus;
                    widget->onBlur      = onWidgetBlur;
                    widget->onValidate  = onWidgetValidate;
                    widget->onKeyPress  = onWidgetKeyPress;
                    widget->onKeyDown   = onWidgetKeyDown;
                    widget->onKeyUp     = onWidgetKeyUp;
                    widget->onMouseDown = onWidgetMouseDown;
                    widget->onMouseUp   = onWidgetMouseUp;
                    widget->onMouseMove = onWidgetMouseMove;
                    widget->onScroll    = onWidgetScroll;
                }

                // Track block type for End handling
                const WgtIfaceT *ctrlIface = wgtGetIface(wgtTypeName);
                bool isCtrlContainer = ctrlIface && ctrlIface->isContainer;

                if (isCtrlContainer && nestDepth < BAS_MAX_FRM_NESTING) {
                    // Push the widget for now; the Layout property hasn't
                    // been parsed yet. It will be applied when we see it
                    // via containerLayout[] below.
                    parentStack[nestDepth++] = widget;

                    if (blockDepth < BAS_MAX_FRM_NESTING) {
                        isContainer[blockDepth++] = true;
                    }
                } else {
                    if (blockDepth < BAS_MAX_FRM_NESTING) {
                        isContainer[blockDepth++] = false;
                    }
                }
            }

            continue;
        }

        // "End"
        if (strcasecmp(trimmed, "End") == 0) {
            if (inMenu) {
                menuNestDepth--;
                curMenuItem = NULL;

                if (menuNestDepth <= 0) {
                    menuNestDepth = 0;
                    inMenu = false;
                }

                if (blockDepth > 0) {
                    blockDepth--;
                }

                continue;
            }

            if (blockDepth > 0) {
                blockDepth--;

                // Only decrement parent nesting for containers (Form/Frame)
                if (isContainer[blockDepth] && nestDepth > 0) {
                    nestDepth--;
                }
            }

            current = NULL;
            continue;
        }

        // Property assignment: Key = Value
        char key[BAS_MAX_CTRL_NAME];
        char value[BAS_MAX_FRM_LINE_LEN];
        parseFrmLine(trimmed, key, value);

        if (key[0] == '\0' || !form) {
            continue;
        }

        if (curMenuItem) {
            // Menu item properties
            char *text = value;

            if (text[0] == '"') {
                text++;
                int32_t len = (int32_t)strlen(text);

                if (len > 0 && text[len - 1] == '"') {
                    text[len - 1] = '\0';
                }
            }

            if (strcasecmp(key, "Caption") == 0)      { snprintf(curMenuItem->caption, sizeof(curMenuItem->caption), "%s", text); }
            else if (strcasecmp(key, "Checked") == 0)    { curMenuItem->checked = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0); }
            else if (strcasecmp(key, "RadioCheck") == 0) { curMenuItem->radioCheck = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0); }
            else if (strcasecmp(key, "Enabled") == 0)  { curMenuItem->enabled = (strcasecmp(text, "True") == 0 || strcasecmp(text, "-1") == 0 || strcasecmp(text, "False") != 0); }
        } else if (current) {
            // Control array index is stored on the struct, not as a widget property
            if (strcasecmp(key, "Index") == 0) {
                current->index = atoi(value);
                continue;
            }

            // HelpTopic is stored on BasControlT, not on the widget
            if (strcasecmp(key, "HelpTopic") == 0) {
                char *text = value;
                if (text[0] == '"') { text++; }
                int32_t tlen = (int32_t)strlen(text);
                if (tlen > 0 && text[tlen - 1] == '"') { text[tlen - 1] = '\0'; }
                snprintf(current->helpTopic, sizeof(current->helpTopic), "%s", text);
                continue;
            }

            // Layout property on a container: replace the parentStack entry
            // with a layout box inside the container widget.
            // NOTE: only do this for non-default layouts (HBox, WrapBox).
            // VBox is the default for Frame, so no wrapper needed.
            if (strcasecmp(key, "Layout") == 0 && current && current->widget && nestDepth > 0) {
                char *text = value;
                if (text[0] == '"') { text++; }
                int32_t tlen = (int32_t)strlen(text);
                if (tlen > 0 && text[tlen - 1] == '"') { text[tlen - 1] = '\0'; }
                if (strcasecmp(text, "VBox") != 0) {
                    parentStack[nestDepth - 1] = basFormRtCreateContentBox(current->widget, text);
                }
                continue;
            }

            BasValueT val;

            if (value[0] == '"') {
                int32_t vlen = (int32_t)strlen(value);

                if (vlen >= 2 && value[vlen - 1] == '"') {
                    value[vlen - 1] = '\0';
                }

                val = basValStringFromC(value + 1);
            } else if (strcasecmp(value, "True") == 0) {
                val = basValBool(true);
            } else if (strcasecmp(value, "False") == 0) {
                val = basValBool(false);
            } else {
                val = basValLong(atoi(value));
            }

            basFormRtSetProp(rt, current, key, val);
            basValRelease(&val);
        } else if (nestDepth > 0) {
            // Form-level property -- strip quotes from string values
            char *text = value;

            if (text[0] == '"') {
                text++;
                int32_t len = (int32_t)strlen(text);

                if (len > 0 && text[len - 1] == '"') {
                    text[len - 1] = '\0';
                }
            }

            if (strcasecmp(key, "Caption") == 0) {
                dvxSetTitle(rt->ctx, form->window, text);
            } else if (strcasecmp(key, "Width") == 0) {
                form->frmWidth = atoi(value);
            } else if (strcasecmp(key, "Height") == 0) {
                form->frmHeight = atoi(value);
            } else if (strcasecmp(key, "Left") == 0) {
                form->frmLeft = atoi(value);
            } else if (strcasecmp(key, "Top") == 0) {
                form->frmTop = atoi(value);
            } else if (strcasecmp(key, "Resizable") == 0) {
                form->frmResizable = (strcasecmp(text, "True") == 0);
                form->frmHasResizable = true;
            } else if (strcasecmp(key, "Centered") == 0) {
                form->frmCentered = (strcasecmp(text, "True") == 0);
            } else if (strcasecmp(key, "AutoSize") == 0) {
                form->frmAutoSize = (strcasecmp(text, "True") == 0);
            } else if (strcasecmp(key, "Layout") == 0) {
                snprintf(form->frmLayout, sizeof(form->frmLayout), "%s", text);
            } else if (strcasecmp(key, "HelpTopic") == 0) {
                snprintf(form->helpTopic, sizeof(form->helpTopic), "%s", text);
            }
        }
    }

    // Apply accumulated form-level properties
    if (form) {
        // Ensure content box exists even if form has no controls
        if (!form->contentBox && form->root) {
            form->contentBox = basFormRtCreateContentBox(form->root, form->frmLayout);
        }

        // Build menu bar from accumulated menu items
        int32_t menuCount = (int32_t)arrlen(menuItems);

        if (menuCount > 0 && form->window) {
            MenuBarT *bar = wmAddMenuBar(form->window);

            if (bar) {
                #define MENU_ID_BASE 10000
                MenuT *menuStack[16];
                memset(menuStack, 0, sizeof(menuStack));

                for (int32_t i = 0; i < menuCount; i++) {
                    TempMenuItemT *mi = &menuItems[i];
                    bool isSep        = (mi->caption[0] == '-' && (mi->caption[1] == '\0' || mi->caption[1] == '-'));
                    bool isSubParent  = (i + 1 < menuCount && menuItems[i + 1].level > mi->level);

                    if (mi->level == 0) {
                        // Top-level menu header
                        menuStack[0] = wmAddMenu(bar, mi->caption);
                    } else if (isSep && mi->level > 0 && menuStack[mi->level - 1]) {
                        // Separator
                        wmAddMenuSeparator(menuStack[mi->level - 1]);
                    } else if (isSubParent && mi->level > 0 && menuStack[mi->level - 1]) {
                        // Submenu parent
                        menuStack[mi->level] = wmAddSubMenu(menuStack[mi->level - 1], mi->caption);
                    } else if (mi->level > 0 && menuStack[mi->level - 1]) {
                        // Regular menu item
                        int32_t id = MENU_ID_BASE + i;

                        if (mi->radioCheck) {
                            wmAddMenuRadioItem(menuStack[mi->level - 1], mi->caption, id, mi->checked);
                        } else if (mi->checked) {
                            wmAddMenuCheckItem(menuStack[mi->level - 1], mi->caption, id, true);
                        } else {
                            wmAddMenuItem(menuStack[mi->level - 1], mi->caption, id);
                        }

                        if (!mi->enabled) {
                            wmMenuItemSetEnabled(bar, id, false);
                        }

                        // Store ID-to-name mapping for event dispatch
                        BasMenuIdMapT map;
                        memset(&map, 0, sizeof(map));
                        map.id = id;
                        snprintf(map.name, BAS_MAX_CTRL_NAME, "%s", mi->name);
                        arrput(form->menuIdMap, map);
                        form->menuIdMapCount = (int32_t)arrlen(form->menuIdMap);
                    }
                }

                form->window->onMenu = onFormMenu;
            }
        }

        arrfree(menuItems);
        menuItems = NULL;

        // Call Resize on widgets that need it (e.g. PictureBox) so the
        // internal bitmap matches the layout size. Width/Height aren't
        // known until properties are parsed, so this must happen after
        // loading but before dvxFitWindow.
        for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
            if (!form->controls[i]->widget) {
                continue;
            }

            const WgtIfaceT *ifc = form->controls[i]->iface;

            if (ifc) {
                WidgetT *wgt = form->controls[i]->widget;

                for (int32_t m = 0; m < ifc->methodCount; m++) {
                    if (strcasecmp(ifc->methods[m].name, "Resize") == 0 &&
                        ifc->methods[m].sig == WGT_SIG_INT_INT &&
                        wgt->minW > 0 && wgt->minH > 0) {
                        ((void (*)(WidgetT *, int32_t, int32_t))ifc->methods[m].fn)(wgt, wgt->minW, wgt->minH);
                        break;
                    }
                }
            }
        }

        // Apply form properties after Resize calls so that
        // PictureBox bitmaps are resized before dvxFitWindow.
        if (form->frmHasResizable) {
            form->window->resizable = form->frmResizable;
        }

        if (form->frmAutoSize) {
            dvxFitWindow(rt->ctx, form->window);
        } else if (form->frmWidth > 0 && form->frmHeight > 0) {
            dvxResizeWindow(rt->ctx, form->window, form->frmWidth, form->frmHeight);
        }

        if (form->frmCentered) {
            form->window->x = (rt->ctx->display.width - form->window->w) / 2;
            form->window->y = (rt->ctx->display.height - form->window->h) / 2;
        } else if (form->frmLeft > 0 || form->frmTop > 0) {
            form->window->x = form->frmLeft;
            form->window->y = form->frmTop;
        }
    }

    // Cache the .frm source for reload after unload
    if (form) {
        bool cached = false;

        for (int32_t i = 0; i < rt->frmCacheCount; i++) {
            if (strcasecmp(rt->frmCache[i].formName, form->name) == 0) {
                cached = true;
                break;
            }
        }

        if (!cached) {
            BasFrmCacheT entry;
            memset(&entry, 0, sizeof(entry));
            snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", form->name);
            entry.frmSource    = (char *)malloc(sourceLen + 1);
            entry.frmSourceLen = sourceLen;

            if (entry.frmSource) {
                memcpy(entry.frmSource, source, sourceLen);
                entry.frmSource[sourceLen] = '\0';
            }

            arrput(rt->frmCache, entry);
            rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);
        }
    }

    // Allocate per-form variable storage and run init code.
    // This must happen AFTER controls are created (so init code can
    // reference controls by name) but BEFORE Form_Load fires.
    if (form && rt->module && rt->module->formVarInfo) {
        for (int32_t j = 0; j < rt->module->formVarInfoCount; j++) {
            if (strcasecmp(rt->module->formVarInfo[j].formName, form->name) != 0) {
                continue;
            }

            if (!form->formVars) {
                int32_t vc = rt->module->formVarInfo[j].varCount;

                if (vc > 0) {
                    form->formVars     = (BasValueT *)calloc(vc, sizeof(BasValueT));
                    form->formVarCount = vc;
                }
            }

            int32_t initAddr = rt->module->formVarInfo[j].initCodeAddr;

            if (initAddr >= 0 && rt->vm) {
                basVmSetCurrentForm(rt->vm, form);
                basVmSetCurrentFormVars(rt->vm, form->formVars, form->formVarCount);
                basVmCallSub(rt->vm, initAddr);
                basVmSetCurrentForm(rt->vm, NULL);
                basVmSetCurrentFormVars(rt->vm, NULL, 0);
            }

            break;
        }
    }

    // Fire the Load event now that the form and controls are ready
    if (form) {
        basFormRtFireEvent(rt, form, form->name, "Load");

        // Auto-refresh Data controls that have no MasterSource (masters/standalone).
        // Detail controls are refreshed automatically by the master-detail cascade
        // when the master's Reposition event fires.
        for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
            BasControlT *dc = form->controls[i];

            if (strcasecmp(dc->typeName, "Data") != 0 || !dc->widget) {
                continue;
            }

            // Skip details — they'll be refreshed by the cascade
            const char *ms = NULL;

            if (dc->iface) {
                for (int32_t p = 0; p < dc->iface->propCount; p++) {
                    if (strcasecmp(dc->iface->props[p].name, "MasterSource") == 0 && dc->iface->props[p].getFn) {
                        ms = ((const char *(*)(const WidgetT *))dc->iface->props[p].getFn)(dc->widget);
                        break;
                    }
                }
            }

            if (ms && ms[0]) {
                continue;
            }

            wgtDataCtrlRefresh(dc->widget);
            updateBoundControls(form, dc);
            refreshDetailControls(form, dc);
        }
    }

    return form;
}


int32_t basFormRtMsgBox(void *ctx, const char *message, int32_t flags, const char *title) {
    BasFormRtT *rt = (BasFormRtT *)ctx;

    return dvxMessageBox(rt->ctx, (title && title[0]) ? title : "DVX BASIC", message, flags);
}


void basFormRtRegisterCfm(BasFormRtT *rt, const char *formName, const uint8_t *data, int32_t dataLen) {
    if (!rt || !formName || !data || dataLen <= 0) {
        return;
    }

    BasCfmCacheT entry;
    snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", formName);
    entry.data    = (uint8_t *)malloc(dataLen);
    entry.dataLen = dataLen;
    memcpy(entry.data, data, dataLen);
    arrput(rt->cfmCache, entry);
    rt->cfmCacheCount = (int32_t)arrlen(rt->cfmCache);
}


void basFormRtRegisterFrm(BasFormRtT *rt, const char *formName, const char *source, int32_t sourceLen) {
    if (!rt || !formName || !source || sourceLen <= 0) {
        return;
    }

    BasFrmCacheT entry;
    snprintf(entry.formName, BAS_MAX_CTRL_NAME, "%s", formName);
    entry.frmSource    = (char *)malloc(sourceLen + 1);
    entry.frmSourceLen = sourceLen;
    memcpy(entry.frmSource, source, sourceLen);
    entry.frmSource[sourceLen] = '\0';
    arrput(rt->frmCache, entry);
    rt->frmCacheCount = (int32_t)arrlen(rt->frmCache);
}


void basFormRtRemoveCtrl(void *ctx, void *formRef, const char *ctrlName) {
    (void)ctx;
    BasFormT *form = (BasFormT *)formRef;

    if (!form || !ctrlName) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
        if (strcasecmp(form->controls[i]->name, ctrlName) == 0) {
            BasControlT *ctrl = form->controls[i];

            // Destroy the widget
            if (ctrl->widget) {
                wgtDestroy(ctrl->widget);
            }

            free(ctrl);
            arrdel(form->controls, i);
        
            return;
        }
    }
}


void basFormRtRunSimple(BasFormRtT *rt) {
    if (!rt || !rt->vm) {
        dvxLog("basFormRtRunSimple: no rt or vm");
        return;
    }

    BasVmT *vm = rt->vm;
    basVmSetStepLimit(vm, BAS_VM_DEFAULT_STEP_SLICE);

    BasVmResultE result;

    do {
        result = basVmRun(vm);

        if (result == BAS_VM_STEP_LIMIT) {
            if (!dvxUpdate(rt->ctx)) {
                break;
            }
        } else if (result == BAS_VM_YIELDED) {
            // DoEvents returned, continue
        } else if (result == BAS_VM_ERROR) {
            const char *errMsg = basVmGetError(vm);
            char buf[512];
            snprintf(buf, sizeof(buf), "Runtime error:\n%s", errMsg ? errMsg : "Unknown error");
            dvxMessageBox(rt->ctx, "Error", buf, 0);
            break;
        } else {
            break;
        }
    } while (1);

    // VB-style event loop: keep alive while forms are open
    if (result == BAS_VM_HALTED) {
        basFormRtEventLoop(rt);
    }
}


void basFormRtSetEvent(void *ctx, void *ctrlRef, const char *eventName, const char *handlerName) {
    (void)ctx;
    BasControlT *ctrl = (BasControlT *)ctrlRef;

    if (!ctrl || !eventName || !handlerName) {
        return;
    }

    // Check for existing override on this event and update it
    for (int32_t i = 0; i < ctrl->eventOverrideCount; i++) {
        if (strcasecmp(ctrl->eventOverrides[i].eventName, eventName) == 0) {
            snprintf(ctrl->eventOverrides[i].handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName);
            return;
        }
    }

    // Add new override
    if (ctrl->eventOverrideCount < BAS_MAX_EVENT_OVERRIDES) {
        BasEventOverrideT *ov = &ctrl->eventOverrides[ctrl->eventOverrideCount++];
        snprintf(ov->eventName, BAS_MAX_CTRL_NAME, "%s", eventName);
        snprintf(ov->handlerName, BAS_MAX_CTRL_NAME, "%s", handlerName);
    }
}


void basFormRtSetProp(void *ctx, void *ctrlRef, const char *propName, BasValueT value) {
    BasFormRtT *rt = (BasFormRtT *)ctx;
    BasControlT *ctrl = (BasControlT *)ctrlRef;

    if (!ctrl) {
        return;
    }

    // Menu item properties
    if (ctrl->menuId > 0 && ctrl->form && ctrl->form->window && ctrl->form->window->menuBar) {
        MenuBarT *bar = ctrl->form->window->menuBar;

        if (strcasecmp(propName, "Checked") == 0) {
            wmMenuItemSetChecked(bar, ctrl->menuId, basValIsTruthy(value));
            return;
        }

        if (strcasecmp(propName, "Enabled") == 0) {
            wmMenuItemSetEnabled(bar, ctrl->menuId, basValIsTruthy(value));
            return;
        }

        return;
    }

    if (!ctrl->widget) {
        return;
    }

    // Form-level property assignment uses the window and BasFormT
    if (ctrl->form && ctrl == &ctrl->form->formCtrl) {
        WindowT *win  = ctrl->form->window;
        BasFormT *frm = ctrl->form;

        if (strcasecmp(propName, "Caption") == 0) {
            BasStringT *s = basValFormatString(value);
            if (win) { dvxSetTitle(rt->ctx, win, s->data); }
            basStringUnref(s);
            return;
        }

        if (strcasecmp(propName, "Visible") == 0) {
            if (win) {
                if (basValIsTruthy(value)) {
                    dvxShowWindow(rt->ctx, win);
                } else {
                    dvxHideWindow(rt->ctx, win);
                }
            }
            return;
        }

        if (strcasecmp(propName, "Width") == 0 && win) {
            dvxResizeWindow(rt->ctx, win, (int32_t)basValToNumber(value), win->h);
            return;
        }

        if (strcasecmp(propName, "Height") == 0 && win) {
            dvxResizeWindow(rt->ctx, win, win->w, (int32_t)basValToNumber(value));
            return;
        }

        if (strcasecmp(propName, "Left") == 0 && win) {
            dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
            win->x = (int32_t)basValToNumber(value);
            dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
            return;
        }

        if (strcasecmp(propName, "Top") == 0 && win) {
            dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
            win->y = (int32_t)basValToNumber(value);
            dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
            return;
        }

        if (strcasecmp(propName, "Resizable") == 0 && win) {
            dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
            win->resizable = basValIsTruthy(value);
            dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
            return;
        }

        if (strcasecmp(propName, "AutoSize") == 0) {
            frm->frmAutoSize = basValIsTruthy(value);
            if (frm->frmAutoSize && win) {
                dvxFitWindow(rt->ctx, win);
            }
            return;
        }

        if (strcasecmp(propName, "Centered") == 0 && win) {
            frm->frmCentered = basValIsTruthy(value);
            if (frm->frmCentered) {
                dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
                win->x = (rt->ctx->display.width - win->w) / 2;
                win->y = (rt->ctx->display.height - win->h) / 2;
                dirtyListAdd(&rt->ctx->dirty, win->x, win->y, win->w, win->h);
            }
            return;
        }

        return;
    }

    // Common properties
    if (setCommonProp(ctrl, propName, value)) {
        return;
    }

    // "Caption" and "Text": pass directly to the widget (all widgets
    // strdup their text internally).
    if (strcasecmp(propName, "Caption") == 0 || strcasecmp(propName, "Text") == 0) {
        BasStringT *s = basValFormatString(value);
        wgtSetText(ctrl->widget, s->data);
        basStringUnref(s);
        return;
    }

    // Help topic
    if (strcasecmp(propName, "HelpTopic") == 0) {
        BasStringT *s = basValFormatString(value);
        snprintf(ctrl->helpTopic, BAS_MAX_CTRL_NAME, "%s", s->data);
        basStringUnref(s);
        return;
    }

    // Data binding properties (stored on BasControlT, not on the widget)
    if (strcasecmp(propName, "DataSource") == 0) {
        BasStringT *s = basValFormatString(value);
        snprintf(ctrl->dataSource, BAS_MAX_CTRL_NAME, "%s", s->data);
        basStringUnref(s);
        return;
    }

    if (strcasecmp(propName, "DataField") == 0) {
        BasStringT *s = basValFormatString(value);
        snprintf(ctrl->dataField, BAS_MAX_CTRL_NAME, "%s", s->data);
        basStringUnref(s);
        return;
    }

    // Interface descriptor properties
    if (ctrl->iface) {
        if (setIfaceProp(ctrl->iface, ctrl->widget, propName, value)) {
            return;
        }
    }
}


void basFormRtShowForm(void *ctx, void *formRef, bool modal) {
    BasFormRtT *rt   = (BasFormRtT *)ctx;
    BasFormT   *form = (BasFormT *)formRef;

    if (!form || !form->window) {
        return;
    }

    dvxShowWindow(rt->ctx, form->window);
    dvxRaiseWindow(rt->ctx, form->window);

    if (form->frmAutoSize) {
        dvxFitWindow(rt->ctx, form->window);
    }

    if (modal) {
        rt->ctx->modalWindow = form->window;
    }
}


void basFormRtUnloadForm(void *ctx, void *formRef) {
    BasFormRtT *rt   = (BasFormRtT *)ctx;
    BasFormT   *form = (BasFormT *)formRef;

    if (!form) {
        return;
    }

    // QueryUnload: give the form a chance to cancel
    if (basFormRtFireEventWithCancel(rt, form, form->name, "QueryUnload")) {
        return;
    }

    basFormRtFireEvent(rt, form, form->name, "Unload");

    // Release per-form variables
    if (form->formVars) {
        for (int32_t i = 0; i < form->formVarCount; i++) {
            basValRelease(&form->formVars[i]);
        }

        free(form->formVars);
        form->formVars     = NULL;
        form->formVarCount = 0;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
        free(form->controls[i]);
    }

    arrfree(form->controls);
    form->controls = NULL;

    if (form->window) {
        if (rt->ctx->modalWindow == form->window) {
            rt->ctx->modalWindow = NULL;
        }

        dvxDestroyWindow(rt->ctx, form->window);
        form->window     = NULL;
        form->root       = NULL;
        form->contentBox = NULL;
    }

    int32_t idx = -1;

    for (int32_t i = 0; i < (int32_t)arrlen(rt->forms); i++) {
        if (rt->forms[i] == form) {
            idx = i;
            break;
        }
    }

    if (idx >= 0 && idx < (int32_t)arrlen(rt->forms)) {
        free(form);
        arrdel(rt->forms, idx);
    
    }
}


const char *basInputBox2(const char *title, const char *prompt, const char *defaultText) {
    if (!sFormRt) {
        return "";
    }

    static char buf[512];
    buf[0] = '\0';

    if (dvxInputBox(sFormRt->ctx, title, prompt, defaultText, buf, sizeof(buf))) {
        return buf;
    }

    return "";
}


int32_t basIntInput(const char *title, const char *prompt, int32_t defaultVal, int32_t minVal, int32_t maxVal) {
    if (!sFormRt) {
        return defaultVal;
    }

    int32_t result = defaultVal;
    dvxIntInputBox(sFormRt->ctx, title, prompt, defaultVal, minVal, maxVal, 1, &result);
    return result;
}


static const BasProcEntryT *basModuleFindProc(const BasModuleT *mod, const char *name) {
    if (!mod || !mod->procs || !name) {
        return NULL;
    }

    for (int32_t i = 0; i < mod->procCount; i++) {
        if (strcasecmp(mod->procs[i].name, name) == 0) {
            return &mod->procs[i];
        }
    }

    return NULL;
}


int32_t basPromptSave(const char *title) {
    if (!sFormRt) {
        return 2;
    }

    return dvxPromptSave(sFormRt->ctx, title);
}


static BasValueT callCommonMethod(BasControlT *ctrl, const char *methodName, BasValueT *args, int32_t argc) {
    (void)args;
    (void)argc;

    if (strcasecmp(methodName, "SetFocus") == 0) {
        wgtSetFocused(ctrl->widget);
        return zeroValue();
    }

    if (strcasecmp(methodName, "Refresh") == 0) {
        wgtInvalidatePaint(ctrl->widget);
        return zeroValue();
    }

    return zeroValue();
}


void CommAttach(int32_t handle, const char *termCtrlName, int32_t channel, int32_t encrypt) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot || !sFormRt || !termCtrlName) {
        return;
    }

    // Find the terminal widget by control name on any loaded form
    WidgetT *termWidget = NULL;

    for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
        BasFormT *form = sFormRt->forms[i];

        for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) {
            if (strcasecmp(form->controls[j]->name, termCtrlName) == 0) {
                termWidget = form->controls[j]->widget;
                break;
            }
        }

        if (termWidget) {
            break;
        }
    }

    if (!termWidget) {
        return;
    }

    slot->termChannel = channel;
    slot->termEncrypt = (encrypt != 0);

    wgtAnsiTermSetComm(termWidget, slot, commTermRead, commTermWrite);
    slot->attachedTerm = termWidget;

    // Set idle callback for automatic polling
    if (sFormRt->ctx) {
        sFormRt->ctx->idleCallback = commIdlePoll;
        sFormRt->ctx->idleCtx      = NULL;
    }
}


void CommClose(int32_t handle) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot) {
        return;
    }

    if (slot->attachedTerm) {
        wgtAnsiTermSetComm(slot->attachedTerm, NULL, NULL, NULL);
    }

    if (sCommApi.close && slot->link) {
        sCommApi.close(slot->link);
    }

    // Free the slot's ~512KB of channel buffers; leave a NULL hole at
    // this index so the handle number doesn't get reused for a different
    // connection until a fresh CommOpen fills the hole.
    free(slot);
    sCommSlots[handle - 1] = NULL;
}


void CommDetach(int32_t handle) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot || !slot->attachedTerm) {
        return;
    }

    wgtAnsiTermSetComm(slot->attachedTerm, NULL, NULL, NULL);
    slot->attachedTerm = NULL;
}


static CommSlotT *commGetSlot(int32_t handle) {
    if (handle < 1 || handle > (int32_t)arrlen(sCommSlots)) {
        return NULL;
    }

    CommSlotT *slot = sCommSlots[handle - 1];
    return (slot && slot->active) ? slot : NULL;
}


int32_t CommHandshake(int32_t handle) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot || !sCommApi.handshake) {
        return 0;
    }

    return (sCommApi.handshake(slot->link) == 0) ? -1 : 0;
}


// Idle callback: polls all active secLink connections
static void commIdlePoll(void *ctx) {
    (void)ctx;

    int32_t n = (int32_t)arrlen(sCommSlots);

    for (int32_t i = 0; i < n; i++) {
        CommSlotT *slot = sCommSlots[i];

        if (slot && slot->active && slot->link && sCommApi.poll) {
            sCommApi.poll(slot->link);
        }
    }
}


int32_t CommIsReady(int32_t handle) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot || !sCommApi.isReady) {
        return 0;
    }

    return sCommApi.isReady(slot->link) ? -1 : 0;
}


// SecLink receive callback: routes data into per-channel ring buffers
static void commOnRecv(void *ctx, const uint8_t *data, int len, uint8_t channel) {
    CommSlotT    *slot = (CommSlotT *)ctx;
    CommChanBufT *cb   = &slot->channels[channel & 0x7F];

    for (int i = 0; i < len; i++) {
        int32_t next = (cb->head + 1) % SECLINK_CHAN_BUF_SIZE;

        if (next == cb->tail) {
            break;
        }

        cb->data[cb->head] = data[i];
        cb->head = next;
    }
}


int32_t CommOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) {
    if (!commResolveApi()) {
        return 0;
    }

    // Reuse an empty slot (hole from a previous close) if possible.
    // Otherwise grow the array by one pointer slot.
    int32_t handle = 0;
    int32_t n      = (int32_t)arrlen(sCommSlots);

    for (int32_t i = 0; i < n; i++) {
        if (!sCommSlots[i]) {
            handle = i + 1;
            break;
        }
    }

    if (handle == 0) {
        arrput(sCommSlots, NULL);
        handle = (int32_t)arrlen(sCommSlots);
    }

    CommSlotT *slot = (CommSlotT *)calloc(1, sizeof(CommSlotT));

    if (!slot) {
        return 0;
    }

    char parityChar = 'N';

    if (parity && parity[0]) {
        parityChar = parity[0];
    }

    slot->link = sCommApi.open(com, baud, dataBits, parityChar, stopBits, handshake, commOnRecv, slot);

    if (!slot->link) {
        free(slot);
        return 0;
    }

    slot->active = true;
    sCommSlots[handle - 1] = slot;
    return handle;
}


int32_t CommPending(int32_t handle) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot || !sCommApi.getPending) {
        return 0;
    }

    return sCommApi.getPending(slot->link);
}


int32_t CommPoll(int32_t handle) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot || !sCommApi.poll) {
        return 0;
    }

    return sCommApi.poll(slot->link);
}


const char *CommRecv(int32_t handle, int32_t channel) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot || channel < 0 || channel >= SECLINK_NUM_CHANNELS) {
        return "";
    }

    CommChanBufT *cb = &slot->channels[channel];
    static char   buf[SECLINK_CHAN_BUF_SIZE];
    int32_t       count = 0;

    while (count < (int32_t)sizeof(buf) - 1 && cb->tail != cb->head) {
        buf[count++] = cb->data[cb->tail];
        cb->tail     = (cb->tail + 1) % SECLINK_CHAN_BUF_SIZE;
    }

    buf[count] = '\0';
    return buf;
}


static bool commResolveApi(void) {
    if (sCommApiResolved) {
        return sCommApi.open != NULL;
    }

    sCommApiResolved = true;
    sCommApi.open       = dlsym(NULL, "_secLinkOpen");
    sCommApi.close      = dlsym(NULL, "_secLinkClose");
    sCommApi.handshake  = dlsym(NULL, "_secLinkHandshake");
    sCommApi.isReady    = dlsym(NULL, "_secLinkIsReady");
    sCommApi.poll       = dlsym(NULL, "_secLinkPoll");
    sCommApi.send       = dlsym(NULL, "_secLinkSend");
    sCommApi.sendBuf    = dlsym(NULL, "_secLinkSendBuf");
    sCommApi.getPending = dlsym(NULL, "_secLinkGetPending");

    return sCommApi.open != NULL;
}


int32_t CommSend(int32_t handle, const char *data, int32_t channel, int32_t encrypt) {
    CommSlotT *slot = commGetSlot(handle);

    if (!slot || !data || !sCommApi.sendBuf) {
        return 0;
    }

    int32_t len = (int32_t)strlen(data);

    if (len == 0) {
        return -1;
    }

    int rc = sCommApi.sendBuf(slot->link, (const uint8_t *)data, len, (uint8_t)channel, encrypt != 0);
    return (rc == 0) ? -1 : 0;
}


// AnsiTerm read callback: drains the terminal channel's ring buffer
static int32_t commTermRead(void *ctx, uint8_t *buf, int32_t maxLen) {
    CommSlotT    *slot = (CommSlotT *)ctx;
    CommChanBufT *cb   = &slot->channels[slot->termChannel];
    int32_t       count = 0;

    while (count < maxLen && cb->tail != cb->head) {
        buf[count++] = cb->data[cb->tail];
        cb->tail     = (cb->tail + 1) % SECLINK_CHAN_BUF_SIZE;
    }

    return count;
}


// AnsiTerm write callback: sends keystrokes on the terminal channel
static int32_t commTermWrite(void *ctx, const uint8_t *data, int32_t len) {
    CommSlotT *slot = (CommSlotT *)ctx;

    if (!slot->link || !sCommApi.send) {
        return 0;
    }

    int rc = sCommApi.send(slot->link, data, len, (uint8_t)slot->termChannel, slot->termEncrypt, false);
    return (rc == 0) ? len : 0;
}


// Create a DVX widget by type name using its registered API.
// The API's create function signature varies by widget type, so
// we call with sensible defaults for each creation pattern.
// For INT_INT types (e.g. PictureBox), hintW/hintH override the
// default createArgs if non-zero (so the bitmap matches the .frm size).

WidgetT *createWidget(const char *wgtTypeName, WidgetT *parent) {
    const void *api = wgtGetApi(wgtTypeName);

    if (!api) {
        return NULL;
    }

    // Determine creation signature from the widget interface descriptor.
    const WgtIfaceT *iface = wgtGetIface(wgtTypeName);
    uint8_t sig = iface ? iface->createSig : WGT_CREATE_PARENT;

    typedef WidgetT *(*CreateParentFnT)(WidgetT *);
    typedef WidgetT *(*CreateParentTextFnT)(WidgetT *, const char *);
    typedef WidgetT *(*CreateParentIntFnT)(WidgetT *, int32_t);
    typedef WidgetT *(*CreateParentIntIntFnT)(WidgetT *, int32_t, int32_t);
    typedef WidgetT *(*CreateParentIntIntIntFnT)(WidgetT *, int32_t, int32_t, int32_t);
    typedef WidgetT *(*CreateParentIntBoolFnT)(WidgetT *, int32_t, bool);
    typedef WidgetT *(*CreateParentBoolFnT)(WidgetT *, bool);

    switch (sig) {
        case WGT_CREATE_PARENT_TEXT: {
            CreateParentTextFnT fn = *(CreateParentTextFnT *)api;
            return fn(parent, "");
        }
        case WGT_CREATE_PARENT_INT: {
            CreateParentIntFnT fn = *(CreateParentIntFnT *)api;
            return fn(parent, iface->createArgs[0]);
        }
        case WGT_CREATE_PARENT_INT_INT: {
            CreateParentIntIntFnT fn = *(CreateParentIntIntFnT *)api;
            return fn(parent, iface->createArgs[0], iface->createArgs[1]);
        }
        case WGT_CREATE_PARENT_INT_INT_INT: {
            CreateParentIntIntIntFnT fn = *(CreateParentIntIntIntFnT *)api;
            return fn(parent, iface->createArgs[0], iface->createArgs[1], iface->createArgs[2]);
        }
        case WGT_CREATE_PARENT_INT_BOOL: {
            CreateParentIntBoolFnT fn = *(CreateParentIntBoolFnT *)api;
            return fn(parent, iface->createArgs[0], (bool)iface->createArgs[1]);
        }
        case WGT_CREATE_PARENT_BOOL: {
            CreateParentBoolFnT fn = *(CreateParentBoolFnT *)api;
            return fn(parent, (bool)iface->createArgs[0]);
        }
        case WGT_CREATE_PARENT_DATA: {
            // create(parent, NULL, 0, 0, 0) -- empty widget, load content later via properties
            typedef WidgetT *(*CreateDataFnT)(WidgetT *, uint8_t *, int32_t, int32_t, int32_t);
            CreateDataFnT fn = *(CreateDataFnT *)api;
            return fn(parent, NULL, 0, 0, 0);
        }
        default: {
            CreateParentFnT fn = *(CreateParentFnT *)api;
            return fn(parent);
        }
    }
}


static void fireCtrlEvent(BasFormRtT *rt, BasControlT *ctrl, const char *eventName, const BasValueT *args, int32_t argCount) {
    // Build final argument list (prepend Index for control arrays).  The
    // array is sized to the actual arg count so any number of event args
    // passes through intact; the +1 is for the Index prefix.
    BasValueT       *allArgs       = NULL;
    const BasValueT *finalArgs     = args;
    int32_t          finalArgCount = argCount;

    if (ctrl->index >= 0) {
        allArgs = (BasValueT *)malloc(sizeof(BasValueT) * (argCount + 1));

        if (!allArgs) {
            return;
        }

        allArgs[0] = basValLong(ctrl->index);

        for (int32_t i = 0; i < argCount; i++) {
            allArgs[i + 1] = args[i];
        }

        finalArgs     = allArgs;
        finalArgCount = argCount + 1;
    }

    // Check for event override (SetEvent)
    for (int32_t i = 0; i < ctrl->eventOverrideCount; i++) {
        if (strcasecmp(ctrl->eventOverrides[i].eventName, eventName) != 0) {
            continue;
        }

        // Override found: call the named SUB directly
        if (!rt->vm || !rt->module) {
            return;
        }

        const BasProcEntryT *proc = basModuleFindProc(rt->module, ctrl->eventOverrides[i].handlerName);

        if (!proc || proc->isFunction) {
            free(allArgs);
            return;
        }

        BasFormT  *prevForm     = rt->currentForm;
        BasValueT *prevVars     = rt->vm->currentFormVars;
        int32_t    prevVarCount = rt->vm->currentFormVarCount;

        rt->currentForm = ctrl->form;
        basVmSetCurrentForm(rt->vm, ctrl->form);
        basVmSetCurrentFormVars(rt->vm, ctrl->form->formVars, ctrl->form->formVarCount);

        if (finalArgCount > 0 && finalArgs) {
            basVmCallSubWithArgs(rt->vm, proc->codeAddr, finalArgs, finalArgCount);
        } else {
            basVmCallSub(rt->vm, proc->codeAddr);
        }

        rt->currentForm = prevForm;
        basVmSetCurrentForm(rt->vm, prevForm);
        basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);
        free(allArgs);
        return;
    }

    // No override -- fall back to naming convention (CtrlName_EventName)
    if (finalArgCount > 0 && finalArgs) {
        basFormRtFireEventArgs(rt, ctrl->form, ctrl->name, eventName, finalArgs, finalArgCount);
    } else {
        basFormRtFireEvent(rt, ctrl->form, ctrl->name, eventName);
    }

    free(allArgs);
}


static BasValueT getCommonProp(BasControlT *ctrl, const char *propName, bool *handled) {
    *handled = true;

    if (strcasecmp(propName, "Name") == 0) {
        return basValStringFromC(ctrl->name);
    }

    if (strcasecmp(propName, "Left") == 0) {
        return basValLong(ctrl->widget->x);
    }

    if (strcasecmp(propName, "Top") == 0) {
        return basValLong(ctrl->widget->y);
    }

    if (strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "MinWidth") == 0) {
        return basValLong(ctrl->widget->w);
    }

    if (strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "MinHeight") == 0) {
        return basValLong(ctrl->widget->h);
    }

    if (strcasecmp(propName, "MaxWidth") == 0) {
        return basValLong(ctrl->widget->maxW ? (ctrl->widget->maxW & WGT_SIZE_VAL_MASK) : 0);
    }

    if (strcasecmp(propName, "MaxHeight") == 0) {
        return basValLong(ctrl->widget->maxH ? (ctrl->widget->maxH & WGT_SIZE_VAL_MASK) : 0);
    }

    if (strcasecmp(propName, "Weight") == 0) {
        return basValLong(ctrl->widget->weight);
    }

    if (strcasecmp(propName, "Visible") == 0) {
        return basValBool(ctrl->widget->visible);
    }

    if (strcasecmp(propName, "Enabled") == 0) {
        return basValBool(ctrl->widget->enabled);
    }

    if (strcasecmp(propName, "TabIndex") == 0) {
        return basValLong(0);
    }

    if (strcasecmp(propName, "BackColor") == 0) {
        return basValLong((int32_t)ctrl->widget->bgColor);
    }

    if (strcasecmp(propName, "ForeColor") == 0) {
        return basValLong((int32_t)ctrl->widget->fgColor);
    }

    *handled = false;
    return zeroValue();
}


// Look up a property in the interface descriptor and call its
// getter, marshaling the return value to BasValueT.

static BasValueT getIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, bool *handled) {
    *handled = false;

    for (int32_t i = 0; i < iface->propCount; i++) {
        if (strcasecmp(iface->props[i].name, propName) != 0) {
            continue;
        }

        *handled = true;
        const WgtPropDescT *p = &iface->props[i];

        if (!p->getFn) {
            return zeroValue();
        }

        switch (p->type) {
            case WGT_IFACE_STRING: {
                const char *s = ((const char *(*)(WidgetT *))p->getFn)(w);
                return basValStringFromC(s ? s : "");
            }

            case WGT_IFACE_INT: {
                int32_t v = ((int32_t (*)(const WidgetT *))p->getFn)(w);
                return basValLong(v);
            }

            case WGT_IFACE_BOOL: {
                bool v = ((bool (*)(const WidgetT *))p->getFn)(w);
                return basValBool(v);
            }

            case WGT_IFACE_FLOAT: {
                float v = ((float (*)(const WidgetT *))p->getFn)(w);
                return basValSingle(v);
            }
        }

        return zeroValue();
    }

    return zeroValue();
}


int32_t HelpCompile(const char *inputFile, const char *outputFile) {
    if (!sHlpcResolved) {
        sHlpcResolved = true;
        sHlpcCompile  = dlsym(NULL, "_hlpcCompile");
    }

    if (!sHlpcCompile || !inputFile || !outputFile) {
        return 0;
    }

    const char *inputs[1];
    inputs[0] = inputFile;

    int32_t rc = sHlpcCompile(inputs, 1, outputFile, NULL, NULL, 1, NULL, NULL);
    return (rc == 0) ? -1 : 0;
}


void HelpView(const char *hlpFile) {
    if (!sShellLoadResolved) {
        sShellLoadResolved = true;
        sShellLoadApp      = dlsym(NULL, "_shellLoadAppWithArgs");
    }

    if (!sShellLoadApp || !sFormRt || !sFormRt->ctx || !hlpFile) {
        return;
    }

    // If hlpFile has no directory component, resolve it against the calling
    // app's directory. This matches the convention where apps bundle their
    // help file alongside the .app.
    char resolved[DVX_MAX_PATH];
    bool hasDir = strchr(hlpFile, '/') != NULL || strchr(hlpFile, '\\') != NULL || (hlpFile[0] && hlpFile[1] == ':');

    if (!hasDir && sFormRt->vm && sFormRt->vm->appPath[0]) {
        snprintf(resolved, sizeof(resolved), "%s" DVX_PATH_SEP "%s", sFormRt->vm->appPath, hlpFile);
    } else {
        snprintf(resolved, sizeof(resolved), "%s", hlpFile);
    }

    sShellLoadApp(sFormRt->ctx, DVX_HELP_VIEWER_APP_LITERAL, resolved);
}


static void onFormActivate(WindowT *win) {
    if (!sFormRt) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
        BasFormT *form = sFormRt->forms[i];

        if (form->window == win) {
            basFormRtFireEvent(sFormRt, form, form->name, "Activate");
            return;
        }
    }
}


// Called when the user closes a BASIC form's window. Fires the
// Form_Unload event and stops the VM so the program exits.

static void onFormClose(WindowT *win) {
    if (!sFormRt) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
        BasFormT *form = sFormRt->forms[i];

        if (form->window == win) {
            // QueryUnload: if Cancel is set, abort the close
            if (basFormRtFireEventWithCancel(sFormRt, form, form->name, "QueryUnload")) {
                return;
            }

            basFormRtFireEvent(sFormRt, form, form->name, "Unload");

            // Release per-form variables
            if (form->formVars) {
                for (int32_t j = 0; j < form->formVarCount; j++) {
                    basValRelease(&form->formVars[j]);
                }

                free(form->formVars);
                form->formVars     = NULL;
                form->formVarCount = 0;
            }

            // Free control resources
            for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) {
                free(form->controls[j]);
            }

            arrfree(form->controls);
            form->controls = NULL;

            // Destroy the window
            if (sFormRt->ctx->modalWindow == win) {
                sFormRt->ctx->modalWindow = NULL;
            }

            dvxDestroyWindow(sFormRt->ctx, win);

            // Remove from form list and free the form
            free(form);
            arrdel(sFormRt->forms, i);

            // If no forms left, stop the VM
            if (arrlen(sFormRt->forms) == 0 && sFormRt->vm) {
                sFormRt->vm->running = false;
            }

            return;
        }
    }
}


static void onFormDeactivate(WindowT *win) {
    if (!sFormRt) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
        BasFormT *form = sFormRt->forms[i];

        if (form->window == win) {
            basFormRtFireEvent(sFormRt, form, form->name, "Deactivate");
            return;
        }
    }
}


// onFormMenu -- dispatch menu clicks as ControlName_Click events
// ============================================================

static void onFormMenu(WindowT *win, int32_t menuId) {
    if (!sFormRt) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
        BasFormT *form = sFormRt->forms[i];

        if (form->window == win) {
            for (int32_t j = 0; j < form->menuIdMapCount; j++) {
                if (form->menuIdMap[j].id == menuId) {
                    basFormRtFireEvent(sFormRt, form, form->menuIdMap[j].name, "Click");
                    return;
                }
            }

            return;
        }
    }
}


static void onFormResize(WindowT *win, int32_t newW, int32_t newH) {
    // Let the widget system re-evaluate scrollbars for the new size
    widgetOnResize(win, newW, newH);

    if (!sFormRt) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
        BasFormT *form = sFormRt->forms[i];

        if (form->window == win) {
            basFormRtFireEvent(sFormRt, form, form->name, "Resize");
            return;
        }
    }
}


static void onWidgetBlur(WidgetT *w) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        fireCtrlEvent(rt, ctrl, "LostFocus", NULL, 0);
    }

    // Write-back: if this control is data-bound, update the Data control's
    // cache and persist to the database
    if (ctrl->dataSource[0] && ctrl->dataField[0] && ctrl->widget && ctrl->form) {
        for (int32_t i = 0; i < (int32_t)arrlen(ctrl->form->controls); i++) {
            BasControlT *dc = ctrl->form->controls[i];

            if (strcasecmp(dc->typeName, "Data") == 0 &&
                strcasecmp(dc->name, ctrl->dataSource) == 0 &&
                dc->widget) {
                const char *text = wgtGetText(ctrl->widget);

                if (text) {
                    wgtDataCtrlSetField(dc->widget, ctrl->dataField, text);
                    wgtDataCtrlUpdate(dc->widget);
                }

                break;
            }
        }
    }
}


static void onWidgetChange(WidgetT *w) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        // Data controls fire "Reposition", update bound controls, and cascade to details
        if (strcasecmp(ctrl->typeName, "Data") == 0) {
            updateBoundControls(ctrl->form, ctrl);
            fireCtrlEvent(rt, ctrl, "Reposition", NULL, 0);
            refreshDetailControls(ctrl->form, ctrl);
            return;
        }

        // Timer widgets fire "Timer" event, everything else fires "Change"
        const char *evtName = (strcasecmp(ctrl->typeName, "Timer") == 0) ? "Timer" : "Change";
        fireCtrlEvent(rt, ctrl, evtName, NULL, 0);
    }
}


static void onWidgetClick(WidgetT *w) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        fireCtrlEvent(rt, ctrl, "Click", NULL, 0);
    }
}


static void onWidgetDblClick(WidgetT *w) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        fireCtrlEvent(rt, ctrl, "DblClick", NULL, 0);
    }
}


static void onWidgetFocus(WidgetT *w) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        fireCtrlEvent(rt, ctrl, "GotFocus", NULL, 0);
    }
}


static void onWidgetKeyDown(WidgetT *w, int32_t keyCode, int32_t shift) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        BasValueT args[2];
        args[0] = basValLong(keyCode);
        args[1] = basValLong(shift);
        fireCtrlEvent(rt, ctrl, "KeyDown", args, 2);
    }
}


static void onWidgetKeyPress(WidgetT *w, int32_t keyAscii) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        BasValueT args[1];
        args[0] = basValLong(keyAscii);
        fireCtrlEvent(rt, ctrl, "KeyPress", args, 1);
    }
}


static void onWidgetKeyUp(WidgetT *w, int32_t keyCode, int32_t shift) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        BasValueT args[2];
        args[0] = basValLong(keyCode);
        args[1] = basValLong(shift);
        fireCtrlEvent(rt, ctrl, "KeyUp", args, 2);
    }
}


static void onWidgetMouseDown(WidgetT *w, int32_t button, int32_t x, int32_t y) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        BasValueT args[3];
        args[0] = basValLong(button);
        args[1] = basValLong(x);
        args[2] = basValLong(y);
        fireCtrlEvent(rt, ctrl, "MouseDown", args, 3);
    }
}


static void onWidgetMouseMove(WidgetT *w, int32_t button, int32_t x, int32_t y) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        BasValueT args[3];
        args[0] = basValLong(button);
        args[1] = basValLong(x);
        args[2] = basValLong(y);
        fireCtrlEvent(rt, ctrl, "MouseMove", args, 3);
    }
}


static void onWidgetMouseUp(WidgetT *w, int32_t button, int32_t x, int32_t y) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        BasValueT args[3];
        args[0] = basValLong(button);
        args[1] = basValLong(x);
        args[2] = basValLong(y);
        fireCtrlEvent(rt, ctrl, "MouseUp", args, 3);
    }
}


static void onWidgetScroll(WidgetT *w, int32_t delta) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (rt) {
        BasValueT args[1];
        args[0] = basValLong(delta);
        fireCtrlEvent(rt, ctrl, "Scroll", args, 1);
    }
}


static bool onWidgetValidate(WidgetT *w) {
    BasControlT *ctrl = (BasControlT *)w->userData;

    if (!ctrl || !ctrl->form || !ctrl->form->vm) {
        return true;
    }

    BasFormRtT *rt = (BasFormRtT *)ctrl->form->vm->ui.ctx;

    if (!rt || !rt->module) {
        return true;
    }

    // Look for Data1_Validate(Cancel As Integer) handler
    char handlerName[MAX_EVENT_NAME_LEN];
    snprintf(handlerName, sizeof(handlerName), "%s_Validate", ctrl->name);
    const BasProcEntryT *proc = basModuleFindProc(rt->module, handlerName);

    if (!proc || proc->isFunction) {
        return true;
    }

    // Pass Cancel = 0 (False). If the handler sets it to non-zero, cancel.
    BasValueT cancelArg = basValLong(0);

    BasFormT  *prevForm     = rt->currentForm;
    BasValueT *prevVars     = rt->vm->currentFormVars;
    int32_t    prevVarCount = rt->vm->currentFormVarCount;

    rt->currentForm = ctrl->form;
    basVmSetCurrentForm(rt->vm, ctrl->form);
    basVmSetCurrentFormVars(rt->vm, ctrl->form->formVars, ctrl->form->formVarCount);

    BasValueT outCancel = basValLong(0);

    if (proc->paramCount == 1) {
        basVmCallSubWithArgsOut(rt->vm, proc->codeAddr, &cancelArg, 1, &outCancel, 1);
    } else {
        basVmCallSub(rt->vm, proc->codeAddr);
    }

    rt->currentForm = prevForm;
    basVmSetCurrentForm(rt->vm, prevForm);
    basVmSetCurrentFormVars(rt->vm, prevVars, prevVarCount);

    // Non-zero Cancel means abort the write
    return !basValIsTruthy(outCancel);
}


static int32_t parseFileFilters(const char *filter, FileFilterT **outFilters, char *buf, int32_t bufSize) {
    *outFilters = NULL;

    if (!filter || !filter[0]) {
        FileFilterT *out = (FileFilterT *)calloc(1, sizeof(FileFilterT));

        if (!out) {
            return 0;
        }

        out[0].label = "All Files (*.*)";
        *outFilters  = out;
        return 1;
    }

    // No pipe and no parentheses = old-style single pattern, wrap it
    if (!strchr(filter, '|') && !strchr(filter, '(')) {
        FileFilterT *out = (FileFilterT *)calloc(2, sizeof(FileFilterT));

        if (!out) {
            return 0;
        }

        snprintf(buf, bufSize, "%s (%s)", filter, filter);
        out[0].label = buf;
        out[1].label = "All Files (*.*)";
        *outFilters  = out;
        return 2;
    }

    // First pass: count pipe-delimited tokens
    int32_t     tokCount = 1;
    const char *cp       = filter;

    while (*cp) {
        if (*cp == '|') {
            tokCount++;
        }

        cp++;
    }

    FileFilterT *out = (FileFilterT *)calloc(tokCount, sizeof(FileFilterT));

    if (!out) {
        return 0;
    }

    snprintf(buf, bufSize, "%s", filter);
    int32_t count = 0;
    char   *p     = buf;

    while (*p) {
        char *pipe = strchr(p, '|');

        if (pipe) {
            *pipe = '\0';
        }

        out[count].label = p;
        count++;
        p = pipe ? pipe + 1 : p + strlen(p);
    }

    if (count <= 0) {
        out[0].label = "All Files (*.*)";
        count        = 1;
    }

    *outFilters = out;
    return count;
}


static void parseFrmLine(const char *line, char *key, char *value) {
    key[0]   = '\0';
    value[0] = '\0';

    line = dvxSkipWs(line);

    int32_t ki = 0;

    while (*line && *line != '=' && *line != ' ' && *line != '\t' && ki < BAS_MAX_CTRL_NAME - 1) {
        key[ki++] = *line++;
    }

    key[ki] = '\0';

    line = dvxSkipWs(line);

    if (*line == '=') {
        line++;
    }

    line = dvxSkipWs(line);

    int32_t vi = 0;

    while (*line && *line != '\r' && *line != '\n' && vi < BAS_MAX_FRM_LINE_LEN - 1) {
        value[vi++] = *line++;
    }

    value[vi] = '\0';

    while (vi > 0 && (value[vi - 1] == ' ' || value[vi - 1] == '\t')) {
        value[--vi] = '\0';
    }
}


// refreshDetailControls -- cascade master-detail: refresh detail Data controls
static void refreshDetailControls(BasFormT *form, BasControlT *masterCtrl) {
    if (!masterCtrl->widget) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
        BasControlT *ctrl = form->controls[i];

        if (strcasecmp(ctrl->typeName, "Data") != 0 || !ctrl->widget || !ctrl->iface) {
            continue;
        }

        // Read MasterSource, MasterField, DetailField from the widget's interface
        const char *ms = NULL;
        const char *mf = NULL;

        for (int32_t p = 0; p < ctrl->iface->propCount; p++) {
            const WgtPropDescT *pd = &ctrl->iface->props[p];

            if (!pd->getFn) {
                continue;
            }

            if (strcasecmp(pd->name, "MasterSource") == 0) {
                ms = ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget);
            } else if (strcasecmp(pd->name, "MasterField") == 0) {
                mf = ((const char *(*)(const WidgetT *))pd->getFn)(ctrl->widget);
            }
        }

        if (!ms || !ms[0] || strcasecmp(ms, masterCtrl->name) != 0) {
            continue;
        }

        // Get the master's current value for MasterField
        const char *val = "";

        if (mf && mf[0]) {
            val = wgtDataCtrlGetField(masterCtrl->widget, mf);
        }

        // Set the filter value and refresh the detail
        wgtDataCtrlSetMasterValue(ctrl->widget, val);
        wgtDataCtrlRefresh(ctrl->widget);

        // Update bound controls for this detail
        updateBoundControls(form, ctrl);
    }
}


int32_t ResAddFile(const char *path, const char *name, int32_t type, const char *srcFile) {
    if (!path || !name || !srcFile) {
        return 0;
    }

    int32_t fileSize = 0;
    uint8_t *buf     = (uint8_t *)platformReadFile(srcFile, &fileSize);

    if (!buf || fileSize <= 0) {
        free(buf);
        return 0;
    }

    int32_t result = dvxResAppend(path, name, (uint32_t)type, buf, (uint32_t)fileSize) == 0 ? -1 : 0;
    free(buf);
    return result;
}


int32_t ResAddText(const char *path, const char *name, const char *text) {
    if (!path || !name) {
        return 0;
    }

    if (!text) {
        text = "";
    }

    return dvxResAppend(path, name, DVX_RES_TEXT, text, (uint32_t)strlen(text) + 1) == 0 ? -1 : 0;
}


void ResClose(int32_t handle) {
    int32_t idx = handle - 1;

    if (idx < 0 || idx >= RES_MAX_HANDLES || !sResHandles[idx]) {
        return;
    }

    dvxResClose(sResHandles[idx]);
    sResHandles[idx] = NULL;
}


int32_t ResCount(int32_t handle) {
    int32_t idx = handle - 1;

    if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx]) {
        return (int32_t)sResHandles[idx]->entryCount;
    }

    return 0;
}


int32_t ResExtract(const char *path, const char *name, const char *outFile) {
    if (!path || !name || !outFile) {
        return 0;
    }

    DvxResHandleT *h = dvxResOpen(path);

    if (!h) {
        return 0;
    }

    uint32_t size = 0;
    void    *data = dvxResRead(h, name, &size);
    dvxResClose(h);

    if (!data) {
        return 0;
    }

    FILE *f = fopen(outFile, "wb");

    if (!f) {
        free(data);
        return 0;
    }

    int32_t result = (fwrite(data, 1, size, f) == size) ? -1 : 0;
    fclose(f);
    free(data);
    return result;
}


const char *ResGetText(const char *path, const char *name) {
    static char sBuf[1024];
    sBuf[0] = '\0';

    if (!path || !name) {
        return sBuf;
    }

    DvxResHandleT *h = dvxResOpen(path);

    if (!h) {
        return sBuf;
    }

    uint32_t size = 0;
    void    *data = dvxResRead(h, name, &size);
    dvxResClose(h);

    if (data) {
        uint32_t copyLen = size;

        if (copyLen >= sizeof(sBuf)) {
            copyLen = sizeof(sBuf) - 1;
        }

        memcpy(sBuf, data, copyLen);
        sBuf[copyLen] = '\0';
        free(data);
    }

    return sBuf;
}


const char *ResName(int32_t handle, int32_t index) {
    int32_t idx = handle - 1;

    if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
        index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
        return sResHandles[idx]->entries[index].name;
    }

    return "";
}


// Resolve a type name (VB-style or DVX widget name) to a DVX
// widget type name. First tries wgtFindByBasName for VB names
// like "CommandButton", then falls back to direct widget name.

static const char *resolveTypeName(const char *typeName) {
    // Try VB name first (e.g. "CommandButton" -> "button")
    const char *wgtName = wgtFindByBasName(typeName);

    if (wgtName) {
        return wgtName;
    }

    // Try as direct widget type name (e.g. "button", "slider")
    if (wgtGetApi(typeName)) {
        return typeName;
    }

    return NULL;
}


int32_t ResOpen(const char *path) {
    if (!path) {
        return 0;
    }

    for (int32_t i = 0; i < RES_MAX_HANDLES; i++) {
        if (!sResHandles[i]) {
            sResHandles[i] = dvxResOpen(path);

            if (sResHandles[i]) {
                return i + 1;
            }

            return 0;
        }
    }

    return 0;
}


int32_t ResRemove(const char *path, const char *name) {
    if (!path || !name) {
        return 0;
    }

    return dvxResRemove(path, name) == 0 ? -1 : 0;
}


int32_t ResSize(int32_t handle, int32_t index) {
    int32_t idx = handle - 1;

    if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
        index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
        return (int32_t)sResHandles[idx]->entries[index].size;
    }

    return 0;
}


int32_t ResType(int32_t handle, int32_t index) {
    int32_t idx = handle - 1;

    if (idx >= 0 && idx < RES_MAX_HANDLES && sResHandles[idx] &&
        index >= 0 && index < (int32_t)sResHandles[idx]->entryCount) {
        return (int32_t)sResHandles[idx]->entries[index].type;
    }

    return 0;
}


void SerAttach(int32_t com, const char *termCtrlName) {
    if (!serResolveApi() || !sFormRt || !termCtrlName) {
        return;
    }

    int32_t idx = com - 1;

    if (idx < 0 || idx >= RS232_NUM_PORTS) {
        return;
    }

    // Find the terminal widget by control name
    WidgetT *termWidget = NULL;

    for (int32_t i = 0; i < (int32_t)arrlen(sFormRt->forms); i++) {
        BasFormT *form = sFormRt->forms[i];

        for (int32_t j = 0; j < (int32_t)arrlen(form->controls); j++) {
            if (strcasecmp(form->controls[j]->name, termCtrlName) == 0) {
                termWidget = form->controls[j]->widget;
                break;
            }
        }

        if (termWidget) {
            break;
        }
    }

    if (!termWidget) {
        return;
    }

    sSerAttach[idx].com      = idx;
    sSerAttach[idx].term     = termWidget;
    sSerAttach[idx].attached = true;

    wgtAnsiTermSetComm(termWidget, &sSerAttach[idx], serTermRead, serTermWrite);

    if (sFormRt->ctx) {
        sFormRt->ctx->idleCallback = serIdlePoll;
        sFormRt->ctx->idleCtx      = NULL;
    }
}


int32_t SerAvailable(int32_t com) {
    if (!serResolveApi() || !sSerApi.getRxBuffered) {
        return 0;
    }

    return sSerApi.getRxBuffered(com - 1);
}


void SerClose(int32_t com) {
    if (!serResolveApi()) {
        return;
    }

    // Detach terminal if attached
    int32_t idx = com - 1;

    if (idx >= 0 && idx < RS232_NUM_PORTS && sSerAttach[idx].attached) {
        wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL);
        sSerAttach[idx].attached = false;
        sSerAttach[idx].term     = NULL;
    }

    sSerApi.close(com - 1);
}


void SerDetach(int32_t com) {
    int32_t idx = com - 1;

    if (idx < 0 || idx >= RS232_NUM_PORTS || !sSerAttach[idx].attached) {
        return;
    }

    wgtAnsiTermSetComm(sSerAttach[idx].term, NULL, NULL, NULL);
    sSerAttach[idx].attached = false;
    sSerAttach[idx].term     = NULL;
}


void SerFlush(int32_t com) {
    if (!serResolveApi() || !sSerApi.clearRxBuffer) {
        return;
    }

    sSerApi.clearRxBuffer(com - 1);
}


int32_t SerGetBase(int32_t com) {
    if (!serResolveApi() || !sSerApi.getBase) {
        return 0;
    }

    return sSerApi.getBase(com - 1);
}


int32_t SerGetIrq(int32_t com) {
    if (!serResolveApi() || !sSerApi.getIrq) {
        return 0;
    }

    return sSerApi.getIrq(com - 1);
}


int32_t SerGetUart(int32_t com) {
    if (!serResolveApi() || !sSerApi.getUartType) {
        return 0;
    }

    return sSerApi.getUartType(com - 1);
}


// Idle callback for raw serial: polls all attached ports
static void serIdlePoll(void *ctx) {
    (void)ctx;

    for (int32_t i = 0; i < RS232_NUM_PORTS; i++) {
        if (sSerAttach[i].attached && sSerAttach[i].term) {
            wgtAnsiTermPoll(sSerAttach[i].term);
        }
    }
}


int32_t SerOpen(int32_t com, int32_t baud, int32_t dataBits, const char *parity, int32_t stopBits, int32_t handshake) {
    if (!serResolveApi()) {
        return 0;
    }

    char parityChar = (parity && parity[0]) ? parity[0] : 'N';
    int  rc = sSerApi.open(com - 1, baud, dataBits, parityChar, stopBits, handshake);
    return (rc == 0) ? -1 : 0;
}


const char *SerRead(int32_t com) {
    if (!serResolveApi()) {
        return "";
    }

    static char buf[SER_RECV_BUF];
    int32_t n = sSerApi.read(com - 1, buf, (int)(sizeof(buf) - 1));

    if (n <= 0) {
        return "";
    }

    buf[n] = '\0';
    return buf;
}


static bool serResolveApi(void) {
    if (sSerApiResolved) {
        return sSerApi.open != NULL;
    }

    sSerApiResolved = true;
    sSerApi.open          = dlsym(NULL, "_rs232Open");
    sSerApi.close         = dlsym(NULL, "_rs232Close");
    sSerApi.read          = dlsym(NULL, "_rs232Read");
    sSerApi.write         = dlsym(NULL, "_rs232Write");
    sSerApi.writeBuf      = dlsym(NULL, "_rs232WriteBuf");
    sSerApi.getRxBuffered = dlsym(NULL, "_rs232GetRxBuffered");
    sSerApi.clearRxBuffer = dlsym(NULL, "_rs232ClearRxBuffer");
    sSerApi.getUartType   = dlsym(NULL, "_rs232GetUartType");
    sSerApi.getBase       = dlsym(NULL, "_rs232GetBase");
    sSerApi.getIrq        = dlsym(NULL, "_rs232GetIrq");
    sSerApi.setBase       = dlsym(NULL, "_rs232SetBase");
    sSerApi.setIrq        = dlsym(NULL, "_rs232SetIrq");

    return sSerApi.open != NULL;
}


void SerSetBase(int32_t com, int32_t base) {
    if (!serResolveApi() || !sSerApi.setBase) {
        return;
    }

    sSerApi.setBase(com - 1, base);
}


void SerSetIrq(int32_t com, int32_t irq) {
    if (!serResolveApi() || !sSerApi.setIrq) {
        return;
    }

    sSerApi.setIrq(com - 1, irq);
}


// Raw serial terminal callbacks
static int32_t serTermRead(void *ctx, uint8_t *buf, int32_t maxLen) {
    SerAttachT *sa = (SerAttachT *)ctx;

    if (!sSerApi.read) {
        return 0;
    }

    return sSerApi.read(sa->com, (char *)buf, maxLen);
}


static int32_t serTermWrite(void *ctx, const uint8_t *data, int32_t len) {
    SerAttachT *sa = (SerAttachT *)ctx;

    if (!sSerApi.writeBuf) {
        return 0;
    }

    int rc = sSerApi.writeBuf(sa->com, (const char *)data, len);
    return (rc >= 0) ? len : 0;
}


int32_t SerWrite(int32_t com, const char *data) {
    if (!serResolveApi() || !data) {
        return 0;
    }

    int32_t len = (int32_t)strlen(data);

    if (len == 0) {
        return -1;
    }

    int rc = sSerApi.writeBuf(com - 1, data, len);
    return (rc >= 0) ? -1 : 0;
}


static bool setCommonProp(BasControlT *ctrl, const char *propName, BasValueT value) {
    if (strcasecmp(propName, "Left") == 0) {
        ctrl->widget->x = (int32_t)basValToNumber(value);
        wgtInvalidate(ctrl->widget);
        return true;
    }

    if (strcasecmp(propName, "Top") == 0) {
        ctrl->widget->y = (int32_t)basValToNumber(value);
        wgtInvalidate(ctrl->widget);
        return true;
    }

    if (strcasecmp(propName, "Width") == 0 || strcasecmp(propName, "MinWidth") == 0) {
        int32_t w = (int32_t)basValToNumber(value);
        ctrl->widget->minW = wgtPixels(w);
        wgtInvalidate(ctrl->widget);
        return true;
    }

    if (strcasecmp(propName, "Height") == 0 || strcasecmp(propName, "MinHeight") == 0) {
        int32_t h = (int32_t)basValToNumber(value);
        ctrl->widget->minH = wgtPixels(h);
        wgtInvalidate(ctrl->widget);
        return true;
    }

    if (strcasecmp(propName, "MaxWidth") == 0) {
        int32_t mw = (int32_t)basValToNumber(value);
        ctrl->widget->maxW = mw > 0 ? wgtPixels(mw) : 0;
        wgtInvalidate(ctrl->widget);
        return true;
    }

    if (strcasecmp(propName, "MaxHeight") == 0) {
        int32_t mh = (int32_t)basValToNumber(value);
        ctrl->widget->maxH = mh > 0 ? wgtPixels(mh) : 0;
        wgtInvalidate(ctrl->widget);
        return true;
    }

    if (strcasecmp(propName, "Weight") == 0) {
        ctrl->widget->weight = (int32_t)basValToNumber(value);
        wgtInvalidate(ctrl->widget);
        return true;
    }

    if (strcasecmp(propName, "Visible") == 0) {
        wgtSetVisible(ctrl->widget, basValIsTruthy(value));
        return true;
    }

    if (strcasecmp(propName, "Enabled") == 0) {
        wgtSetEnabled(ctrl->widget, basValIsTruthy(value));
        return true;
    }

    if (strcasecmp(propName, "TabIndex") == 0) {
        return true;
    }

    if (strcasecmp(propName, "BackColor") == 0) {
        ctrl->widget->bgColor = (uint32_t)(int32_t)basValToNumber(value);
        wgtInvalidatePaint(ctrl->widget);
        return true;
    }

    if (strcasecmp(propName, "ForeColor") == 0) {
        ctrl->widget->fgColor = (uint32_t)(int32_t)basValToNumber(value);
        wgtInvalidatePaint(ctrl->widget);
        return true;
    }

    return false;
}


// Look up a property in the interface descriptor and call its
// setter, marshaling the BasValueT to the native type.

static bool setIfaceProp(const WgtIfaceT *iface, WidgetT *w, const char *propName, BasValueT value) {
    for (int32_t i = 0; i < iface->propCount; i++) {
        if (strcasecmp(iface->props[i].name, propName) != 0) {
            continue;
        }

        const WgtPropDescT *p = &iface->props[i];

        if (!p->setFn) {
            return true; // read-only, but recognized
        }

        switch (p->type) {
            case WGT_IFACE_STRING: {
                BasStringT *s = basValFormatString(value);
                ((void (*)(WidgetT *, const char *))p->setFn)(w, s->data);
                basStringUnref(s);
                break;
            }

            case WGT_IFACE_ENUM:
                if (p->enumNames && value.type == BAS_TYPE_STRING && value.strVal) {
                    // Map name to index
                    int32_t enumVal = 0;

                    for (int32_t en = 0; p->enumNames[en]; en++) {
                        if (strcasecmp(p->enumNames[en], value.strVal->data) == 0) {
                            enumVal = en;
                            break;
                        }
                    }

                    ((void (*)(WidgetT *, int32_t))p->setFn)(w, enumVal);
                } else {
                    ((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value));
                }
                break;

            case WGT_IFACE_INT:
                ((void (*)(WidgetT *, int32_t))p->setFn)(w, (int32_t)basValToNumber(value));
                break;

            case WGT_IFACE_BOOL:
                ((void (*)(WidgetT *, bool))p->setFn)(w, basValIsTruthy(value));
                break;

            case WGT_IFACE_FLOAT:
                ((void (*)(WidgetT *, float))p->setFn)(w, (float)basValToNumber(value));
                break;
        }

        return true;
    }

    return false;
}


int32_t SQLAffected(int32_t db) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlAffectedRows) {
        return 0;
    }

    return sFormRt->vm->sql.sqlAffectedRows(db);
}


void SQLClose(int32_t db) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlClose) {
        return;
    }

    sFormRt->vm->sql.sqlClose(db);
}


int32_t SQLEof(int32_t rs) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlEof) {
        return -1;
    }

    return sFormRt->vm->sql.sqlEof(rs) ? -1 : 0;
}


const char *SQLError(int32_t db) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlError) {
        return "";
    }

    const char *err = sFormRt->vm->sql.sqlError(db);
    return err ? err : "";
}


int32_t SQLExec(int32_t db, const char *sql) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlExec) {
        return 0;
    }

    return sFormRt->vm->sql.sqlExec(db, sql) ? -1 : 0;
}


const char *SQLField(int32_t rs, const char *colName) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldByName) {
        return "";
    }

    const char *text = sFormRt->vm->sql.sqlFieldByName(rs, colName);
    return text ? text : "";
}


int32_t SQLFieldCount(int32_t rs) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldCount) {
        return 0;
    }

    return sFormRt->vm->sql.sqlFieldCount(rs);
}


double SQLFieldDbl(int32_t rs, int32_t col) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldDbl) {
        return 0.0;
    }

    return sFormRt->vm->sql.sqlFieldDbl(rs, col);
}


int32_t SQLFieldInt(int32_t rs, int32_t col) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldInt) {
        return 0;
    }

    return sFormRt->vm->sql.sqlFieldInt(rs, col);
}


const char *SQLFieldName(int32_t rs, int32_t col) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldName) {
        return "";
    }

    const char *name = sFormRt->vm->sql.sqlFieldName(rs, col);
    return name ? name : "";
}


const char *SQLFieldText(int32_t rs, int32_t col) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFieldText) {
        return "";
    }

    const char *text = sFormRt->vm->sql.sqlFieldText(rs, col);
    return text ? text : "";
}


void SQLFreeResult(int32_t rs) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlFreeResult) {
        return;
    }

    sFormRt->vm->sql.sqlFreeResult(rs);
}


int32_t SQLNext(int32_t rs) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlNext) {
        return 0;
    }

    return sFormRt->vm->sql.sqlNext(rs) ? -1 : 0;
}


int32_t SQLOpen(const char *path) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlOpen) {
        return 0;
    }

    return sFormRt->vm->sql.sqlOpen(path);
}


int32_t SQLQuery(int32_t db, const char *sql) {
    if (!sFormRt || !sFormRt->vm || !sFormRt->vm->sql.sqlQuery) {
        return 0;
    }

    return sFormRt->vm->sql.sqlQuery(db, sql);
}


// updateBoundControls -- sync bound controls from Data control's current record
static void updateBoundControls(BasFormT *form, BasControlT *dataCtrl) {
    if (!dataCtrl->widget) {
        return;
    }

    for (int32_t i = 0; i < (int32_t)arrlen(form->controls); i++) {
        BasControlT *ctrl = form->controls[i];

        if (!ctrl->dataSource[0] || strcasecmp(ctrl->dataSource, dataCtrl->name) != 0) {
            continue;
        }

        if (!ctrl->widget) {
            continue;
        }

        // DBGrid: bind to the Data control widget and refresh
        if (strcasecmp(ctrl->typeName, "DBGrid") == 0) {
            wgtDbGridSetDataWidget(ctrl->widget, dataCtrl->widget);
            wgtDbGridRefresh(ctrl->widget);
            continue;
        }

        // Text-based controls: set text from current record field
        if (ctrl->dataField[0]) {
            const char *val = wgtDataCtrlGetField(dataCtrl->widget, ctrl->dataField);

            if (val) {
                wgtSetText(ctrl->widget, val);
            }
        }
    }
}


static BasValueT zeroValue(void) {
    BasValueT v;
    memset(&v, 0, sizeof(v));
    return v;
}


