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

// basBuild.c -- shared "compile a project into an .app" back end
//
// See basBuild.h for the public contract.  This pipeline used to live
// inline in two places: ideMain.c (the IDE's Make Executable path) and
// bascomp.c (the standalone command-line compiler).  Both implementations
// were essentially identical, so they are consolidated here.
//
// Pipeline:
//   1. Strip comments from every .frm text.
//   2. Serialize the module; release builds deserialize a private copy,
//      strip its debug info, obfuscate form/control names (rewriting the
//      .frm texts too), compact the bytecode and re-serialize.
//   3. Debug builds serialize the debug info.
//   4. Extract the STUB resource from selfPath and write it as outPath.
//   5. Append the resource set in canonical order:
//        BAS_RES_NAME (always, falls back to "BASIC App")
//        BAS_RES_AUTHOR / PUBLISHER / VERSION / COPYRIGHT / DESCRIPTION
//        BAS_RES_ICON32 (project icon, else the NOICON fallback)
//        BAS_RES_HELPFILE (basename only)
//        BAS_RES_MODULE, BAS_RES_DEBUG (debug builds)
//        FORM0, FORM1, ... cut at the end of the outer Begin Form block
//        so the .frm's BASIC code section (already compiled into
//        MODULE) never ships as a resource
//   6. Copy the help file next to outPath.

#include "basBuild.h"
#include "basRes.h"
#include "compiler/compact.h"
#include "compiler/obfuscate.h"
#include "compiler/strip.h"
#include "runtime/serialize.h"
#include "dvxRes.h"
#include "dvxTypes.h"
#include "dvxPlat.h"

#include "stb_ds_wrap.h"

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

// Slack added to a .frm buffer for basStripFrmComments output.
#define BAS_BUILD_STRIP_MARGIN 16

// Longest progress line handed to the log callback.
#define BAS_BUILD_LOG_BUF      128

// Separator used when joining project-relative paths.  '/' is accepted by
// DJGPP as well as every host OS bascomp runs on; DVX_PATH_SEP is not.
#define BAS_BUILD_SEP          "/"

// Function prototypes (alphabetical)
static int32_t     appendText(const char *path, const char *name, const char *value);
const char        *basBuildApp(const char *outPath, const BasBuildSpecT *spec);
static void        buildLog(const BasBuildSpecT *spec, const char *fmt, ...);
static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec);
static int32_t     emitIcon(const char *outPath, const BasBuildSpecT *spec);
static int32_t     emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount);
static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen);
static const char *writeStub(const char *outPath, const BasBuildSpecT *spec);


static int32_t appendText(const char *path, const char *name, const char *value) {
    if (!value || !value[0]) {
        return 0;
    }

    return dvxResAppend(path, name, DVX_RES_TEXT, value, (uint32_t)strlen(value) + 1);
}


const char *basBuildApp(const char *outPath, const BasBuildSpecT *spec) {
    if (!outPath || !spec || !spec->module || !spec->selfPath) {
        return "Invalid build request.";
    }

    // Strip comments from every .frm text.  Comments are source-only and
    // must not ship in the embedded resource for either build mode.
    uint8_t    **frmData = NULL; // stb_ds: stripped form text
    int32_t     *frmLens = NULL; // stb_ds: length of stripped form text
    BasObfFrmT  *obfFrms = NULL; // stb_ds: obfuscated variants (release)

    for (int32_t i = 0; i < spec->frmCount; i++) {
        const char *src = spec->frmSources[i];

        if (!src) {
            continue;
        }

        int32_t  srcLen   = (int32_t)strlen(src);
        int32_t  stripCap = srcLen + BAS_BUILD_STRIP_MARGIN;
        uint8_t *stripped = (uint8_t *)malloc(stripCap);

        if (!stripped) {
            continue;
        }

        int32_t    strippedLen = basStripFrmComments(src, srcLen, stripped, stripCap);
        BasObfFrmT empty       = { NULL, 0 };

        arrput(frmData, stripped);
        arrput(frmLens, strippedLen);
        arrput(obfFrms, empty);
    }

    int32_t     frmCount = (int32_t)arrlen(frmData);
    uint8_t    *modData  = NULL;
    int32_t     modLen   = 0;
    uint8_t    *dbgData  = NULL;
    int32_t     dbgLen   = 0;
    const char *failure  = prepareModule(spec, frmData, frmLens, obfFrms, frmCount, &modData, &modLen, &dbgData, &dbgLen);

    if (!failure) {
        failure = writeStub(outPath, spec);
    }

    if (!failure && emitResources(outPath, spec, modData, modLen, dbgData, dbgLen, frmData, frmLens, obfFrms, frmCount) != 0) {
        failure = "Failed writing resources to output file.";
    }

    if (!failure) {
        failure = copyHelpFile(outPath, spec);
    }

    free(modData);
    free(dbgData);

    for (int32_t i = 0; i < frmCount; i++) {
        free(frmData[i]);
        free(obfFrms[i].data);
    }

    arrfree(frmData);
    arrfree(frmLens);
    arrfree(obfFrms);

    return failure;
}


static void buildLog(const BasBuildSpecT *spec, const char *fmt, ...) {
    if (!spec->log) {
        return;
    }

    char    msg[BAS_BUILD_LOG_BUF];
    va_list ap;

    va_start(ap, fmt);
    vsnprintf(msg, sizeof(msg), fmt, ap);
    va_end(ap);
    spec->log(msg);
}


// Copy the project's help file next to the output app; the HELPFILE
// resource itself names only the basename, which the stub resolves
// relative to the app.
static const char *copyHelpFile(const char *outPath, const BasBuildSpecT *spec) {
    if (!spec->helpFile || !spec->helpFile[0]) {
        return NULL;
    }

    char helpSrc[DVX_MAX_PATH];
    snprintf(helpSrc, sizeof(helpSrc), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->helpFile);

    char outDir[DVX_MAX_PATH];
    snprintf(outDir, sizeof(outDir), "%s", outPath);
    char *sep = platformPathDirEnd(outDir);

    if (sep) {
        *sep = '\0';
    } else {
        outDir[0] = '.';
        outDir[1] = '\0';
    }

    char helpDst[DVX_MAX_PATH];
    snprintf(helpDst, sizeof(helpDst), "%s" BAS_BUILD_SEP "%s", outDir, platformPathBaseName(spec->helpFile));

    if (!platformCopyFile(helpSrc, helpDst)) {
        return "Failed copying help file to output directory.";
    }

    return NULL;
}


// Embed the project icon, or the NOICON fallback carried by the running
// executable when the project names none.
static int32_t emitIcon(const char *outPath, const BasBuildSpecT *spec) {
    char    iconPath[DVX_MAX_PATH];
    void   *iconData = NULL;
    int32_t iconLen  = 0;

    if (spec->iconPath && spec->iconPath[0]) {
        snprintf(iconPath, sizeof(iconPath), "%s" BAS_BUILD_SEP "%s", spec->projectDir, spec->iconPath);
        iconData = platformReadFile(iconPath, &iconLen);

        if (!iconData) {
            // A named icon that cannot be read is a build failure --
            // shipping the app without its ICON32 resource would never
            // tell anyone.
            return -1;
        }
    } else {
        DvxResHandleT *selfRes = dvxResOpen(spec->selfPath);

        if (selfRes) {
            uint32_t size = 0;

            iconData = dvxResRead(selfRes, BAS_RES_NOICON, &size);
            iconLen  = (int32_t)size;
            dvxResClose(selfRes);
        }

        if (!iconData) {
            return 0;
        }
    }

    int32_t rc = dvxResAppend(outPath, BAS_RES_ICON32, DVX_RES_ICON, iconData, (uint32_t)iconLen);
    free(iconData);
    return rc;
}


static int32_t emitResources(const char *outPath, const BasBuildSpecT *spec, const uint8_t *modData, int32_t modLen, const uint8_t *dbgData, int32_t dbgLen, uint8_t **frmData, const int32_t *frmLens, const BasObfFrmT *obfFrms, int32_t frmCount) {
    // Accumulate every append result so a disk-full or I/O failure on any
    // resource is reported instead of being silently treated as success.
    int32_t rc = 0;

    // Project metadata.  Name is required -- fall back to a generic label
    // so the compiled app always has something sensible to display.
    const char *projName = (spec->projName && spec->projName[0]) ? spec->projName : "BASIC App";
    rc |= appendText(outPath, BAS_RES_NAME, projName);

    rc |= appendText(outPath, BAS_RES_AUTHOR,      spec->author);
    rc |= appendText(outPath, BAS_RES_PUBLISHER,   spec->publisher);
    rc |= appendText(outPath, BAS_RES_VERSION,     spec->version);
    rc |= appendText(outPath, BAS_RES_COPYRIGHT,   spec->copyright);
    rc |= appendText(outPath, BAS_RES_DESCRIPTION, spec->description);

    rc |= emitIcon(outPath, spec);

    if (spec->helpFile && spec->helpFile[0]) {
        rc |= appendText(outPath, BAS_RES_HELPFILE, platformPathBaseName(spec->helpFile));
    }

    rc |= dvxResAppend(outPath, BAS_RES_MODULE, DVX_RES_BINARY, modData, (uint32_t)modLen);

    if (dbgData && dbgLen > 0) {
        rc |= dvxResAppend(outPath, BAS_RES_DEBUG, DVX_RES_BINARY, dbgData, (uint32_t)dbgLen);
    }

    // Form resources.  Release builds embed the obfuscated variant when
    // one was produced.  Number the OUTPUT densely with outIdx rather
    // than the source index: skipping an empty form by source index would
    // leave a numbering gap (FORM0, FORM2, ...) and the stub's reader
    // stops at the first missing index, silently dropping every later
    // form.
    int32_t outIdx = 0;

    for (int32_t i = 0; i < frmCount; i++) {
        const uint8_t *data = frmData[i];
        int32_t        len  = frmLens[i];

        if (spec->release && obfFrms[i].data) {
            data = (const uint8_t *)obfFrms[i].data;
            len  = obfFrms[i].len;
        }

        if (!data || len <= 0) {
            continue;
        }

        // The stub's loader scans FORM0..FORM(BAS_MAX_FORM_RESOURCES-1)
        // and stops there, so anything past that cap would be written but
        // unreachable at runtime.  Fail the build loudly instead of
        // shipping forms the app can never load.
        if (outIdx >= BAS_MAX_FORM_RESOURCES) {
            rc |= -1;
            break;
        }

        char    resName[BAS_RES_FORM_NAME_LEN];
        int32_t formLen = basFindFormEndPos((const char *)data, len);

        snprintf(resName, sizeof(resName), BAS_RES_FORM_FMT, (long)outIdx);
        rc |= dvxResAppend(outPath, resName, DVX_RES_BINARY, data, (uint32_t)formLen);
        outIdx++;
    }

    return rc;
}


// Serialize the module (and, for debug builds, its debug info).  Release
// builds run strip -> obfuscate -> compact on a private copy so the
// caller's module stays intact; obfuscation also fills obfFrms.
static const char *prepareModule(const BasBuildSpecT *spec, uint8_t **frmData, int32_t *frmLens, BasObfFrmT *obfFrms, int32_t frmCount, uint8_t **outMod, int32_t *outModLen, uint8_t **outDbg, int32_t *outDbgLen) {
    uint8_t *modData = basModuleSerialize(spec->module, outModLen);

    if (!modData) {
        return "Failed to serialize module.";
    }

    if (!spec->release) {
        *outMod = modData;
        *outDbg = basDebugSerialize(spec->module, outDbgLen);
        return NULL;
    }

    BasModuleT *modCopy = basModuleDeserialize(modData, *outModLen);
    free(modData);

    if (!modCopy) {
        return "Failed to prepare release module.";
    }

    basStripModule(modCopy);
    buildLog(spec, "  stripped debug info");

    if (frmCount > 0) {
        const char **frmTexts = NULL;

        for (int32_t i = 0; i < frmCount; i++) {
            arrput(frmTexts, (const char *)frmData[i]);
        }

        basObfuscateNames(modCopy, frmTexts, frmLens, frmCount, obfFrms);
        arrfree(frmTexts);
        buildLog(spec, "  obfuscated %d form(s)", (int)frmCount);
    }

    int32_t removed = basCompactBytecode(modCopy);

    if (removed > 0) {
        buildLog(spec, "  compacted bytecode (-%d bytes)", (int)removed);
    }

    *outMod = basModuleSerialize(modCopy, outModLen);
    basModuleFree(modCopy);

    if (!*outMod) {
        return "Failed to serialize release module.";
    }

    return NULL;
}


// Extract the STUB resource from the running executable and write it as
// the output file; every resource is appended behind it.
static const char *writeStub(const char *outPath, const BasBuildSpecT *spec) {
    DvxResHandleT *selfRes = dvxResOpen(spec->selfPath);

    if (!selfRes) {
        return "Cannot open own executable to read embedded stub.";
    }

    uint32_t  stubSize = 0;
    void     *stubData = dvxResRead(selfRes, BAS_RES_STUB, &stubSize);
    dvxResClose(selfRes);

    if (!stubData || stubSize == 0) {
        free(stubData);
        return "STUB resource not found in own executable.";
    }

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

    if (!outFile) {
        free(stubData);
        return "Cannot create output file.";
    }

    size_t  stubWritten = fwrite(stubData, 1, stubSize, outFile);
    int32_t stubClose   = fclose(outFile);
    free(stubData);

    if (stubWritten != stubSize || stubClose != 0) {
        return "Failed writing stub to output file.";
    }

    return NULL;
}
