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

// 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/parser.h"
#include "../compiler/symtab.h"
#include "../runtime/vm.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>
#include <unistd.h>

// Initial capacity of the source-concatenation buffer, and the hard
// ceiling its capacity may reach.  The ceiling keeps the int32 capacity
// doubling in concatGrow from overflowing.
#define CONCAT_INITIAL_CAP   8192
#define CONCAT_MAX_CAP       0x40000000 // 1 GiB cap; keeps int32 doubling safe

// Kernel view of the running image on hosted OSes.
#define SELF_EXE_PATH        "/proc/self/exe"

// Function prototypes (alphabetical)
static void buildLog(const char *msg);
static bool concatGrow(char **buf, int32_t *cap, int32_t need);
static const char *selfPath(const char *argv0);
static void usage(void);
int main(int argc, char **argv);


static void buildLog(const char *msg) {
    printf("%s\n", msg);
}


static bool concatGrow(char **buf, int32_t *cap, int32_t need) {
    if (need < 0 || need > CONCAT_MAX_CAP) {
        return false;
    }

    int32_t newCap = *cap;

    while (newCap < need) {
        if (newCap > CONCAT_MAX_CAP / 2) {
            // Doubling would overflow int32; jump straight to need.
            newCap = need;
            break;
        }

        newCap *= 2;
    }

    char *grown = (char *)realloc(*buf, (size_t)newCap);

    if (grown == NULL) {
        return false;
    }

    *buf = grown;
    *cap = newCap;
    return true;
}


// Path of this executable, which carries the embedded STUB and NOICON
// resources.  On a hosted OS argv[0] is whatever the shell typed (a bare
// name when invoked through PATH), so the kernel's view of the running
// image is preferred; DJGPP always passes a full path in argv[0].
static const char *selfPath(const char *argv0) {
    #ifndef __DJGPP__
    if (access(SELF_EXE_PATH, R_OK) == 0) {
        return SELF_EXE_PATH;
    }
    #endif

    return argv0;
}


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 (int32_t 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, "");
    bool optionExplicit = prefsGetBool(prefs, BAS_INI_SECTION_SETTINGS, BAS_INI_KEY_OPTIONEXPLICIT, false);

    // 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");
        goto failFiles;
    }

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

    if (!concatBuf) {
        fprintf(stderr, "Error: out of memory.\n");
        goto failFiles;
    }

    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 (files[i].isForm != (pass == 1)) {
                continue;
            }

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

            if (!srcBuf) {
                fprintf(stderr, "Error: cannot read %s\n", files[i].path);
                goto concatFail;
            }

            const char *code = srcBuf;

            if (files[i].isForm) {
                // Extract form name from "Begin Form <name>"
                char formName[BAS_MAX_IDENT] = "";
                basExtractFormName(srcBuf, formName, BAS_MAX_IDENT);

                // The BASIC code section follows the outer Begin Form block.
                code = srcBuf + basFindFormEndPos(srcBuf, srcLen);

                int32_t codeLen = (int32_t)strlen(code);

                if (!concatGrow(&concatBuf, &concatCap, pos + codeLen + 128)) {
                    fprintf(stderr, "Error: out of memory.\n");
                    free(srcBuf);
                    goto concatFail;
                }

                // 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);

                if (!concatGrow(&concatBuf, &concatCap, pos + codeLen + 2)) {
                    fprintf(stderr, "Error: out of memory.\n");
                    free(srcBuf);
                    goto concatFail;
                }

                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");
        goto concatFail;
    }

    basParserInit(parser, concatBuf, pos);
    parser->optionExplicit = optionExplicit;

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

    free(concatBuf);

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

    if (!mod) {
        fprintf(stderr, "Error: failed to build module.\n");
        goto failFiles;
    }

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

    // Raw .frm texts; the shared build pipeline strips comments and, for
    // release builds, obfuscates them.
    char **frmSources = NULL; // stb_ds: owned form text

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

        char *fdata = platformReadFile(files[i].path, NULL);

        if (fdata) {
            arrput(frmSources, fdata);
        }
    }

    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.projectDir  = projectDir;
    spec.iconPath    = iconPath;
    spec.helpFile    = helpFile;
    spec.selfPath    = selfPath(argv[0]);
    spec.module      = mod;
    spec.frmSources  = (const char *const *)frmSources;
    spec.frmCount    = (int32_t)arrlen(frmSources);
    spec.release     = release;
    spec.log         = buildLog;

    const char *failure = basBuildApp(outputPath, &spec);

    for (int32_t i = 0; i < (int32_t)arrlen(frmSources); i++) {
        free(frmSources[i]);
    }

    arrfree(frmSources);
    basModuleFree(mod);
    arrfree(files);
    prefsClose(prefs);

    if (failure) {
        fprintf(stderr, "Error: %s\n", failure);
        return 1;
    }

    // Report the true on-disk size of the finished app (stub, bytecode,
    // debug info, icon, metadata and every FORMn resource).
    int32_t outBytes = 0;
    FILE   *szf      = fopen(outputPath, "rb");

    if (szf) {
        fseek(szf, 0, SEEK_END);
        long endPos = ftell(szf);

        if (endPos > 0) {
            outBytes = (int32_t)endPos;
        }

        fclose(szf);
    }

    printf("Created %s (%d bytes)\n", outputPath, (int)outBytes);
    return 0;

    // ----- Error cleanup paths -----
    // failFiles: only the files array exists.  Each early error frees its
    // own raw malloc'd buffers (concatBuf, parser) before jumping here.
failFiles:
    arrfree(files);
    prefsClose(prefs);
    return 1;

    // The concat buffer is owned by paths that run before the frm arrays
    // exist; free it here and route to the files-only cleanup.  The caller
    // has already printed an appropriate error message.
concatFail:
    free(concatBuf);
    goto failFiles;
}
