// bascomp.c -- DVX BASIC command-line compiler
//
// Compiles a .dbp project into a standalone .app file.
//
// Usage: BASCOMP project.dbp [-o output.app] [-release]
//
// The project file and all referenced source files (.bas, .frm)
// are loaded relative to the directory containing the .dbp file.
// The stub (basstub.app) is read from the same directory as the
// compiler executable.

#include "../compiler/compact.h"
#include "../compiler/lexer.h"
#include "../compiler/obfuscate.h"
#include "../compiler/parser.h"
#include "../compiler/strip.h"
#include "../compiler/symtab.h"
#include "../compiler/opcodes.h"
#include "../runtime/vm.h"
#include "../runtime/values.h"
#include "../runtime/serialize.h"
#include "../../../../libs/kpunch/libdvx/dvxRes.h"
#include "../../../../libs/kpunch/libdvx/dvxPrefs.h"
#include "../../../../libs/kpunch/libdvx/dvxTypes.h"
#include "../../../../libs/kpunch/libdvx/platform/dvxPlat.h"
#include "../basBuild.h"
#include "../basRes.h"

#include "stb_ds_wrap.h"

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

// Function prototypes (alphabetical)
static void concatGrow(char **buf, int32_t *cap, int32_t need);
static const char *extractFormCode(const char *frmText);
int main(int argc, char **argv);
static void usage(void);


static void concatGrow(char **buf, int32_t *cap, int32_t need) {
    while (*cap < need) {
        *cap *= 2;
    }

    *buf = (char *)realloc(*buf, *cap);
}


static const char *extractFormCode(const char *frmText) {
    if (!frmText) {
        return NULL;
    }

    const char *p = frmText;
    int32_t depth = 0;
    bool inForm = false;

    while (*p) {
        // Skip leading whitespace
        p = dvxSkipWs(p);

        if (strncasecmp(p, "Begin ", 6) == 0) {
            if (!inForm && strncasecmp(p + 6, "Form ", 5) == 0) {
                inForm = true;
            }

            depth++;
        } else if (strncasecmp(p, "End", 3) == 0 && (p[3] == '\0' || p[3] == '\r' || p[3] == '\n' || p[3] == ' ')) {
            depth--;

            if (depth <= 0 && inForm) {
                // Skip past this line
                while (*p && *p != '\n') { p++; }

                if (*p == '\n') { p++; }

                return p;
            }
        }

        // Skip to next line
        while (*p && *p != '\n') { p++; }

        if (*p == '\n') { p++; }
    }

    return NULL;
}


static void usage(void) {
    fprintf(stderr, "DVX BASIC Compiler\n\n");
    fprintf(stderr, "Usage: BASCOMP project.dbp [-o output.app] [-release]\n\n");
    fprintf(stderr, "  project.dbp    DVX BASIC project file\n");
    fprintf(stderr, "  -o output.app  Output file (default: project name + .app)\n");
    fprintf(stderr, "  -release       Strip debug information\n");
}


int main(int argc, char **argv) {
    if (argc < 2) {
        usage();
        return 1;
    }

    const char *dbpPath    = NULL;
    const char *outputPath = NULL;
    bool        release    = false;

    for (int i = 1; i < argc; i++) {
        if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) {
            outputPath = argv[++i];
        } else if (strcmp(argv[i], "-release") == 0) {
            release = true;
        } else if (argv[i][0] != '-') {
            dbpPath = argv[i];
        } else {
            fprintf(stderr, "Unknown option: %s\n", argv[i]);
            usage();
            return 1;
        }
    }

    if (!dbpPath) {
        fprintf(stderr, "Error: no project file specified.\n");
        usage();
        return 1;
    }

    // Load the project file
    PrefsHandleT *prefs = prefsLoad(dbpPath);

    if (!prefs) {
        fprintf(stderr, "Error: cannot open project file: %s\n", dbpPath);
        return 1;
    }

    // Derive project directory
    char projectDir[DVX_MAX_PATH];
    snprintf(projectDir, sizeof(projectDir), "%s", dbpPath);
    char *sep = platformPathDirEnd(projectDir);

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

    // Read project metadata
    const char *projName    = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_NAME, "App");
    const char *author      = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_AUTHOR, "");
    const char *publisher   = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_PUBLISHER, "");
    const char *version     = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_VERSION, "");
    const char *copyright   = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_COPYRIGHT, "");
    const char *description = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_DESCRIPTION, "");
    const char *iconPath    = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_ICON, "");
    const char *helpFile    = prefsGetString(prefs, BAS_INI_SECTION_PROJECT, BAS_INI_KEY_HELPFILE, "");
    const char *startupForm = prefsGetString(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_STARTUPFORM, "");
    (void)startupForm; // used implicitly by stub's basFormRtLoadAllForms

    // Derive output path
    char outBuf[DVX_MAX_PATH];

    if (!outputPath) {
        snprintf(outBuf, sizeof(outBuf), "%s/%s.app", projectDir, projName);
        outputPath = outBuf;
    }

    printf("Project: %s\n", projName);
    printf("Output:  %s (%s)\n", outputPath, release ? "release" : "debug");

    // Collect source files (dynamic array; grows as needed)
    typedef struct {
        char path[DVX_MAX_PATH];
        bool isForm;
    } SrcFileT;

    SrcFileT *files = NULL;

    // Modules
    for (int32_t i = 0; ; i++) {
        char key[16];
        snprintf(key, sizeof(key), "File%d", (int)i);
        const char *val = prefsGetString(prefs, BAS_INI_SECTION_MODULES, key, NULL);

        if (!val) {
            break;
        }

        SrcFileT entry;
        snprintf(entry.path, DVX_MAX_PATH, "%s/%s", projectDir, val);
        entry.isForm = false;
        arrput(files, entry);
    }

    // Forms
    for (int32_t i = 0; ; i++) {
        char key[16];
        snprintf(key, sizeof(key), "File%d", (int)i);
        const char *val = prefsGetString(prefs, BAS_INI_SECTION_FORMS, key, NULL);

        if (!val) {
            break;
        }

        SrcFileT entry;
        snprintf(entry.path, DVX_MAX_PATH, "%s/%s", projectDir, val);
        entry.isForm = true;
        arrput(files, entry);
    }

    int32_t fileCount = (int32_t)arrlen(files);

    if (fileCount == 0) {
        fprintf(stderr, "Error: project has no source files.\n");
        prefsClose(prefs);
        return 1;
    }

    // Concatenate sources (modules first, then form code)
    int32_t concatCap = 8192;
    char   *concatBuf = (char *)malloc(concatCap);

    if (!concatBuf) {
        fprintf(stderr, "Error: out of memory.\n");
        prefsClose(prefs);
        return 1;
    }

    int32_t pos = 0;

    // Pass 0: .bas modules, Pass 1: .frm code sections
    for (int32_t pass = 0; pass < 2; pass++) {
        for (int32_t i = 0; i < fileCount; i++) {
            if (pass == 0 && files[i].isForm)  { continue; }
            if (pass == 1 && !files[i].isForm) { continue; }

            int32_t srcLen = 0;
            char *srcBuf = platformReadFile(files[i].path, &srcLen);

            if (!srcBuf) {
                fprintf(stderr, "Error: cannot read %s\n", files[i].path);
                free(concatBuf);
                prefsClose(prefs);
                return 1;
            }

            const char *code = srcBuf;

            if (files[i].isForm) {
                // Extract form name from "Begin Form <name>"
                char formName[BAS_MAX_SYMBOL_NAME] = "";
                const char *bp = srcBuf;

                while (*bp) {
                    bp = dvxSkipWs(bp);

                    if (strncasecmp(bp, "Begin Form ", 11) == 0) {
                        bp = dvxSkipWs(bp + 11);

                        int32_t ni = 0;

                        while (*bp && *bp != ' ' && *bp != '\t' && *bp != '\r' && *bp != '\n' && ni < BAS_MAX_SYMBOL_NAME - 1) {
                            formName[ni++] = *bp++;
                        }

                        formName[ni] = '\0';
                        break;
                    }

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

                    if (*bp == '\n') { bp++; }
                }

                code = extractFormCode(srcBuf);

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

                int32_t codeLen = (int32_t)strlen(code);
                concatGrow(&concatBuf, &concatCap, pos + codeLen + 128);

                // Inject BEGINFORM directive before form code
                if (formName[0]) {
                    pos += snprintf(concatBuf + pos, concatCap - pos, "BEGINFORM \"%s\"\n", formName);
                }

                memcpy(concatBuf + pos, code, codeLen);
                pos += codeLen;

                if (pos > 0 && concatBuf[pos - 1] != '\n') {
                    concatBuf[pos++] = '\n';
                }

                // Inject ENDFORM directive after form code
                if (formName[0]) {
                    pos += snprintf(concatBuf + pos, concatCap - pos, "ENDFORM\n");
                }
            } else {
                int32_t codeLen = (int32_t)strlen(code);
                concatGrow(&concatBuf, &concatCap, pos + codeLen + 2);

                memcpy(concatBuf + pos, code, codeLen);
                pos += codeLen;

                if (pos > 0 && concatBuf[pos - 1] != '\n') {
                    concatBuf[pos++] = '\n';
                }
            }

            free(srcBuf);
        }
    }

    concatBuf[pos] = '\0';

    // Compile
    printf("Compiling %d file(s)...\n", (int)fileCount);

    BasParserT *parser = (BasParserT *)malloc(sizeof(BasParserT));

    if (!parser) {
        fprintf(stderr, "Error: out of memory.\n");
        free(concatBuf);
        prefsClose(prefs);
        return 1;
    }

    basParserInit(parser, concatBuf, pos);

    if (!basParse(parser)) {
        fprintf(stderr, "Compile error at line %d: %s\n", (int)parser->errorLine, parser->error);
        basParserFree(parser);
        free(parser);
        free(concatBuf);
        prefsClose(prefs);
        return 1;
    }

    free(concatBuf);

    BasModuleT *mod = basParserBuildModule(parser);
    basParserFree(parser);
    free(parser);

    if (!mod) {
        fprintf(stderr, "Error: failed to build module.\n");
        prefsClose(prefs);
        return 1;
    }

    printf("  code: %d bytes, %d procs, %d constants\n", (int)mod->codeLen, (int)mod->procCount, (int)mod->constCount);

    // Strip for release
    if (release) {
        basStripModule(mod);
        printf("  stripped debug info\n");
    }

    // Read all .frm texts up front; they're used for obfuscation and
    // then embedded as FORM0, FORM1, ... resources below.  Parallel
    // dynamic arrays, one entry per form file.
    char    **frmData    = NULL; // stb_ds: strdup'd stripped form text
    int32_t  *frmLens    = NULL; // stb_ds: length of stripped form text

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

        int32_t flen = 0;
        char *fdata = platformReadFile(files[i].path, &flen);

        if (!fdata) {
            continue;
        }

        // Strip comments from the .frm text unconditionally. Comments
        // are source-only; they shouldn't ship in the embedded resource
        // for either debug or release builds.
        int32_t  stripCap = flen + 16;
        uint8_t *stripped = (uint8_t *)malloc(stripCap);

        if (!stripped) {
            free(fdata);
            continue;
        }

        int32_t strippedLen = basStripFrmComments(fdata, flen, stripped, stripCap);
        free(fdata);

        arrput(frmData, (char *)stripped);
        arrput(frmLens, strippedLen);
    }

    int32_t frmCount = (int32_t)arrlen(frmData);

    // Obfuscate form/control names in release mode
    BasObfFrmT *obfFrms = NULL;

    for (int32_t i = 0; i < frmCount; i++) {
        BasObfFrmT empty = { NULL, 0 };
        arrput(obfFrms, empty);
    }

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

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

        basObfuscateNames(mod, frmTexts, frmLens, frmCount, obfFrms);
        arrfree(frmTexts);
        printf("  obfuscated %d form(s)\n", (int)frmCount);
    }

    // Remove OP_LINE instructions and compact the bytecode.
    if (release) {
        int32_t removed = basCompactBytecode(mod);

        if (removed > 0) {
            printf("  compacted bytecode (-%d bytes)\n", (int)removed);
        }
    }

    // Serialize module
    int32_t modLen = 0;
    uint8_t *modData = basModuleSerialize(mod, &modLen);

    if (!modData) {
        fprintf(stderr, "Error: failed to serialize module.\n");
        basModuleFree(mod);
        prefsClose(prefs);
        return 1;
    }

    // Serialize debug info
    int32_t  dbgLen  = 0;
    uint8_t *dbgData = NULL;

    if (!release) {
        dbgData = basDebugSerialize(mod, &dbgLen);
    }

    basModuleFree(mod);

    // Read the stub DXE embedded in our own executable as a resource.
    // The bascomp Makefile appends basstub.app to bascomp post-link
    // via `dvxres add ... STUB binary @basstub.app`, so BASSTUB.APP no
    // longer has to sit alongside the compiler.
    DvxResHandleT *selfRes = dvxResOpen(argv[0]);

    if (!selfRes) {
        fprintf(stderr, "Error: cannot open %s to read embedded stub.\n", argv[0]);
        free(modData);
        free(dbgData);
        prefsClose(prefs);
        return 1;
    }

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

    if (!stubData) {
        fprintf(stderr, "Error: STUB resource not found in %s.\n", argv[0]);
        free(modData);
        free(dbgData);
        prefsClose(prefs);
        return 1;
    }

    // Write stub to output file
    FILE *out = fopen(outputPath, "wb");

    if (!out) {
        fprintf(stderr, "Error: cannot create %s\n", outputPath);
        free(stubData);
        free(modData);
        free(dbgData);
        prefsClose(prefs);
        return 1;
    }

    fwrite(stubData, 1, stubLen, out);
    fclose(out);
    free(stubData);

    // Pick the right form bytes per build mode: release builds use the
    // obfuscated variant when available, debug builds always use the raw
    // stripped text.
    const uint8_t **emitFormData = NULL;
    int32_t        *emitFormLens = NULL;

    for (int32_t fi = 0; fi < frmCount; fi++) {
        if (release && obfFrms[fi].data) {
            arrput(emitFormData, (const uint8_t *)obfFrms[fi].data);
            arrput(emitFormLens, obfFrms[fi].len);
        } else {
            arrput(emitFormData, (const uint8_t *)frmData[fi]);
            arrput(emitFormLens, frmLens[fi]);
        }
    }

    // Resolve icon disk path (if any) against the project directory.
    char iconFullPath[DVX_MAX_PATH];
    const char *iconDiskPath = NULL;

    if (iconPath[0]) {
        snprintf(iconFullPath, sizeof(iconFullPath), "%s/%s", projectDir, iconPath);
        iconDiskPath = iconFullPath;
    }

    // Emit all resources via the shared helper.
    BasBuildSpecT spec;
    memset(&spec, 0, sizeof(spec));
    spec.projName    = projName;
    spec.author      = author;
    spec.publisher   = publisher;
    spec.version     = version;
    spec.copyright   = copyright;
    spec.description = description;
    spec.helpFile    = helpFile;
    spec.iconPath    = iconDiskPath;
    spec.moduleData  = modData;
    spec.moduleLen   = modLen;
    spec.debugData   = dbgData;
    spec.debugLen    = dbgLen;
    spec.formCount   = frmCount;
    spec.formData    = emitFormData;
    spec.formLens    = emitFormLens;

    basBuildEmitResources(outputPath, &spec);

    free(modData);
    free(dbgData);

    // Copy help file to output directory (the HELPFILE resource itself was
    // written by basBuildEmitResources).
    if (helpFile[0]) {
        const char *helpBase = platformPathBaseName(helpFile);

        char helpSrc[DVX_MAX_PATH];
        snprintf(helpSrc, sizeof(helpSrc), "%s/%s", projectDir, helpFile);

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

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

        char helpDst[DVX_MAX_PATH];
        snprintf(helpDst, sizeof(helpDst), "%s/%s", outDir, helpBase);

        int32_t hLen = 0;
        char *hData = platformReadFile(helpSrc, &hLen);

        if (hData) {
            FILE *hf = fopen(helpDst, "wb");

            if (hf) {
                fwrite(hData, 1, hLen, hf);
                fclose(hf);
            }

            free(hData);
        }
    }

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

    arrfree(files);
    arrfree(frmData);
    arrfree(frmLens);
    arrfree(obfFrms);
    arrfree(emitFormData);
    arrfree(emitFormLens);

    prefsClose(prefs);

    printf("Created %s (%d bytes)\n", outputPath, (int)stubLen + (int)modLen);
    return 0;
}
