// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

// basstub.c -- DVX BASIC standalone application stub
//
// Minimal entry point for compiled BASIC applications. Reads the
// MODULE resource (serialized bytecode), FORMn resources (.frm text),
// and optional DEBUG resource from its own DXE file. Creates a VM
// and form runtime from basrt.lib and runs the program.
//
// This file is compiled into basstub.app, which is embedded as a
// resource in dvxbasic.app. "Make Executable" extracts it, renames
// it to the project name, and attaches the compiled resources.

#include "dvxApp.h"
#include "dvxDlg.h"
#include "dvxRes.h"
#include "dvxWgt.h"
#include "shellApp.h"
#include "../basRes.h"
#include "../runtime/vm.h"
#include "../runtime/serialize.h"
#include "../formrt/formrt.h"

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

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

#define STUB_STACK_SIZE 65536

AppDescriptorT appDescriptor = {
    .name          = "BASIC App",
    .hasMainLoop   = true,
    .multiInstance = false,
    .stackSize     = STUB_STACK_SIZE,
    .priority      = TS_PRIORITY_NORMAL
};


// ============================================================
// Callbacks
// ============================================================

static AppContextT *sAc = NULL;


// Function prototypes (alphabetical; appMain last)
void        appShutdown(void);
static bool stubDoEvents(void *ctx);
static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize);
static void stubPrint(void *ctx, const char *text, bool newline);
int32_t     appMain(DxeAppContextT *ctx);


// Resolved by the shell as _appShutdown and called on BOTH graceful reap and
// force-kill, before this app's DXE is unmapped.  On a normal exit appMain
// already ran basFormRtDestroy; on a force-kill it never returns, so this is
// the only path that drops the serial/secLink idle pollers from the shell
// registry -- otherwise they would keep polling this app's freed terminals.
void appShutdown(void) {
    basFormRtSerialShutdown();
}


static bool stubDoEvents(void *ctx) {
    (void)ctx;

    if (!sAc) {
        return false;
    }

    return dvxUpdate(sAc);
}


static bool stubInput(void *ctx, const char *prompt, char *buf, int32_t bufSize) {
    (void)ctx;
    buf[0] = '\0';

    if (!sAc) {
        return false;
    }

    return dvxInputBox(sAc, "Input", prompt, "", buf, bufSize);
}


static void stubPrint(void *ctx, const char *text, bool newline) {
    (void)ctx;
    (void)text;
    (void)newline;
}


int32_t appMain(DxeAppContextT *ctx) {
    sAc = ctx->shellCtx;

    // Open our own resources
    DvxResHandleT *res = dvxResOpen(ctx->appPath);

    if (!res) {
        dvxMessageBox(sAc, "Error", "No compiled module found in this application.", 0);
        return 1;
    }

    // Read app name and update the shell's app record.  dvxResRead returns
    // exactly the stored size with no terminator of its own, so bound every
    // text resource by its size instead of trusting a trailing NUL.
    uint32_t nameSize = 0;
    char *appName = (char *)dvxResRead(res, BAS_RES_NAME, &nameSize);

    if (appName) {
        ShellAppT *app = shellGetApp(ctx->appId);

        if (app) {
            snprintf(app->name, SHELL_APP_NAME_MAX, "%.*s", (int)nameSize, appName);
        }

        free(appName);
    }

    // Set help file path if present
    uint32_t helpNameSize = 0;
    char *helpName = (char *)dvxResRead(res, BAS_RES_HELPFILE, &helpNameSize);

    if (helpName) {
        snprintf(ctx->helpFile, sizeof(ctx->helpFile), "%s" DVX_PATH_SEP "%.*s", ctx->appDir, (int)helpNameSize, helpName);
        free(helpName);
    }

    // Load MODULE resource
    uint32_t modSize = 0;
    uint8_t *modData = (uint8_t *)dvxResRead(res, BAS_RES_MODULE, &modSize);

    if (!modData) {
        dvxMessageBox(sAc, "Error", "MODULE resource not found.", 0);
        dvxResClose(res);
        return 1;
    }

    BasModuleT *mod = basModuleDeserialize(modData, (int32_t)modSize);
    free(modData);

    if (!mod) {
        dvxMessageBox(sAc, "Error", "Failed to deserialize module.", 0);
        dvxResClose(res);
        return 1;
    }

    // Load optional DEBUG resource
    uint32_t dbgSize = 0;
    uint8_t *dbgData = (uint8_t *)dvxResRead(res, BAS_RES_DEBUG, &dbgSize);

    if (dbgData) {
        basDebugDeserialize(mod, dbgData, (int32_t)dbgSize);
        free(dbgData);
    }

    // Create VM
    BasVmT *vm = basVmCreate();

    if (!vm) {
        dvxMessageBox(sAc, "Error", "Out of memory creating the BASIC runtime.", 0);
        basModuleFree(mod);
        dvxResClose(res);
        return 1;
    }

    basVmLoadModule(vm, mod);
    basVmSetPrintCallback(vm, stubPrint, NULL);
    basVmSetInputCallback(vm, stubInput, NULL);
    basVmSetDoEventsCallback(vm, stubDoEvents, NULL);

    // Set app paths.  App.Path is the .app's directory (read-only on CD);
    // App.Config and App.Data are writable subdirectories created on
    // demand.  The IDE does the same split for project debugging, so
    // behavior matches between compiled apps and in-IDE runs.
    snprintf(vm->appPath,   DVX_MAX_PATH, "%s", ctx->appDir);
    snprintf(vm->appConfig, DVX_MAX_PATH, "%s" DVX_PATH_SEP "CONFIG", ctx->appDir);
    snprintf(vm->appData,   DVX_MAX_PATH, "%s" DVX_PATH_SEP "DATA",   ctx->appDir);
    platformMkdirRecursive(vm->appConfig);
    platformMkdirRecursive(vm->appData);

    // Set extern call callbacks (required for DECLARE LIBRARY functions)
    BasExternCallbacksT extCb;
    memset(&extCb, 0, sizeof(extCb));
    extCb.resolveExtern = basExternResolve;
    extCb.callExtern    = basExternCall;
    basVmSetExternCallbacks(vm, &extCb);

    // Create form runtime
    BasFormRtT *rt = basFormRtCreate(sAc, vm, mod);

    if (!rt) {
        dvxMessageBox(sAc, "Error", "Out of memory creating the form runtime.", 0);
        basVmDestroy(vm);
        basModuleFree(mod);
        dvxResClose(res);
        return 1;
    }

    // Register .frm source text for lazy loading
    for (int32_t i = 0; i < BAS_MAX_FORM_RESOURCES; i++) {
        char resName[BAS_RES_FORM_NAME_LEN];
        snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)i);

        uint32_t frmSize = 0;
        char *frmText = (char *)dvxResRead(res, resName, &frmSize);

        if (!frmText) {
            // Stop at the first missing index: basBuild.c numbers the FORMn
            // resources densely (outIdx), so there is never a gap and the
            // first miss means there are no more forms.  Probing all
            // BAS_MAX_FORM_RESOURCES indices would just waste startup lookups.
            break;
        }

        // dvxResRead returns exactly frmSize bytes with no terminator; the
        // form text is not guaranteed NUL-terminated.  Append one so the
        // unbounded scan in basExtractFormName can't read past the buffer.
        char *frmTextZ = (char *)realloc(frmText, frmSize + 1);

        if (!frmTextZ) {
            free(frmText);
            continue;
        }

        frmText          = frmTextZ;
        frmText[frmSize] = '\0';

        // Extract form name from "Begin Form <name>" line.
        char frmName[BAS_MAX_IDENT] = "";
        basExtractFormName(frmText, frmName, BAS_MAX_IDENT);

        if (frmName[0]) {
            basFormRtRegisterFrm(rt, frmName, frmText, (int32_t)frmSize);
        }

        free(frmText);
    }

    dvxResClose(res);

    // Load all cached forms and show the startup form
    basFormRtLoadAllForms(rt, NULL);

    // Run bytecode + VB-style event loop
    basFormRtRunSimple(rt);

    // Cleanup
    basFormRtDestroy(rt);
    basVmDestroy(vm);
    basModuleFree(mod);

    return 0;
}
