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

// serialize.c -- DVX BASIC module serialization
//
// Binary format (all little-endian, no alignment padding):
//
//   Header:
//     magic        4 bytes  "DBXM"
//     version      uint16   1
//     entryPoint   int32
//     globalCount  int32
//     codeLen      int32
//     constCount   int32
//     dataCount    int32
//     procCount    int32
//
//   code[]         codeLen bytes of raw bytecode
//
//   constants[]    constCount entries, each:
//                    len uint16 + len bytes of string data
//
//   dataPool[]     dataCount entries, each:
//                    type uint8 + value (type-dependent)
//
//   procs[]        procCount entries, each:
//                    codeAddr     int32
//                    paramCount   int32
//                    localCount   int32
//                    returnType   uint8
//                    isFunction   uint8
//                    name         uint16 len + bytes
//                    formName     uint16 len + bytes
//
//   formVarInfoCount int32, then that many entries:
//                    formName     uint16 len + bytes
//                    varCount     int32
//                    initCodeAddr int32
//                    initCodeLen  int32
//
//   globalInitCount  int32, then that many entries:
//                    index        int32
//                    dataType     uint8

#include "serialize.h"
#include "../compiler/opcodes.h"

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

// Module file header: 4-byte magic + uint16 version.  A matched pair
// -- read by basModuleDeserialize, written by basModuleSerialize --
// so both sides pull from the same definition.
#define BAS_MOD_MAGIC        "DBXM"
#define BAS_MOD_MAGIC_LEN    4
#define BAS_MOD_VERSION      1

// Fixed header sizes on the wire.  BAS_MOD_HEADER_SIZE is magic(4) +
// version(2) + 6 int32 header fields(24) = 30.  BAS_DBG_HEADER_SIZE is
// the leading int32 debugVarCount.
#define BAS_MOD_HEADER_SIZE  30
#define BAS_DBG_HEADER_SIZE  4

// Minimum on-wire byte size of one entry in each variable-length table.
// Used by readCount and the inline remaining-bytes count checks so that
// no bare integers appear at the call sites.
#define BAS_CONST_MIN_ENTRY       2
#define BAS_DATA_MIN_ENTRY        2
#define BAS_PROC_MIN_ENTRY        18
#define BAS_FORMVAR_MIN_ENTRY     14
#define BAS_GLOBALINIT_MIN_ENTRY  5
#define BAS_DEBUGVAR_MIN_ENTRY    14
#define BAS_BUF_INIT_CAP          4096

// ============================================================
// Write helpers
// ============================================================

typedef struct {
    uint8_t *buf;
    int32_t  len;
    int32_t  cap;
    bool     err;
} BufT;

typedef struct {
    const uint8_t *data;
    int32_t        len;
    int32_t        pos;
    bool           err;
} ReaderT;


// Function prototypes (alphabetical)
void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen);
uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen);
BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen);
void basModuleFree(BasModuleT *mod);
uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen);
static void bufGrow(BufT *b, int32_t need);
static void bufInit(BufT *b);
static void bufWrite(BufT *b, const void *data, int32_t len);
static void bufWriteI32(BufT *b, int32_t v);
static void bufWriteStr(BufT *b, const char *s);
static void bufWriteU16(BufT *b, uint16_t v);
static void bufWriteU8(BufT *b, uint8_t v);
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry);
static double rF64(ReaderT *r);
static int32_t rI32(ReaderT *r);
static bool rOk(ReaderT *r, int32_t need);
static char *rStr(ReaderT *r);
static void rStrInto(ReaderT *r, char *dst, int32_t cap);
static uint16_t rU16(ReaderT *r);
static uint8_t rU8(ReaderT *r);

void basDebugDeserialize(BasModuleT *mod, const uint8_t *data, int32_t dataLen) {
    if (!mod || !data || dataLen < BAS_DBG_HEADER_SIZE) {
        return;
    }

    ReaderT r = { data, dataLen, 0, false };

    // Debug variables
    mod->debugVarCount = readCount(&r, BAS_DEBUGVAR_MIN_ENTRY);

    if (mod->debugVarCount > 0) {
        mod->debugVars = (BasDebugVarT *)calloc(mod->debugVarCount, sizeof(BasDebugVarT));

        if (!mod->debugVars) {
            mod->debugVarCount = 0;
            return;
        }

        for (int32_t i = 0; i < mod->debugVarCount; i++) {
            BasDebugVarT *v = &mod->debugVars[i];
            rStrInto(&r, v->name, BAS_MAX_IDENT);
            rStrInto(&r, v->formName, BAS_MAX_IDENT);
            v->scope     = rU8(&r);
            v->dataType  = rU8(&r);
            v->index     = rI32(&r);
            v->procIndex = rI32(&r);
        }

        // A truncated debug resource would otherwise leave phantom entries
        // (empty names, index 0 aliasing real global slot 0).  Drop to
        // no-debug-info rather than display garbage.
        if (r.err) {
            free(mod->debugVars);
            mod->debugVars     = NULL;
            mod->debugVarCount = 0;
        }
    }

}


uint8_t *basDebugSerialize(const BasModuleT *mod, int32_t *outLen) {
    if (!mod || !outLen) {
        return NULL;
    }

    BufT b;
    bufInit(&b);

    // Debug variables
    bufWriteI32(&b, mod->debugVarCount);

    for (int32_t i = 0; i < mod->debugVarCount; i++) {
        const BasDebugVarT *v = &mod->debugVars[i];
        bufWriteStr(&b, v->name);
        bufWriteStr(&b, v->formName);
        bufWriteU8(&b, v->scope);
        bufWriteU8(&b, v->dataType);
        bufWriteI32(&b, v->index);
        bufWriteI32(&b, v->procIndex);
    }

    if (b.err) {
        free(b.buf);
        return NULL;
    }

    *outLen = b.len;
    return b.buf;
}


BasModuleT *basModuleDeserialize(const uint8_t *data, int32_t dataLen) {
    if (!data || dataLen < BAS_MOD_HEADER_SIZE) {
        return NULL;
    }

    ReaderT r = { data, dataLen, 0, false };

    // Check magic
    if (memcmp(data, BAS_MOD_MAGIC, BAS_MOD_MAGIC_LEN) != 0) {
        return NULL;
    }

    r.pos = BAS_MOD_MAGIC_LEN;

    uint16_t version = rU16(&r);

    if (version != BAS_MOD_VERSION) {
        return NULL;
    }

    BasModuleT *mod = (BasModuleT *)calloc(1, sizeof(BasModuleT));

    if (!mod) {
        return NULL;
    }

    mod->entryPoint  = rI32(&r);
    mod->globalCount = rI32(&r);
    mod->codeLen     = rI32(&r);
    mod->constCount  = rI32(&r);
    mod->dataCount   = rI32(&r);
    mod->procCount   = rI32(&r);

    // The constCount/dataCount/procCount counts live in the fixed header,
    // ahead of the code blob, so their remaining-bytes upper bound cannot
    // be checked until the code has been consumed.  Reject negatives now.
    // globalCount has no payload here, but a negative value would defeat
    // the VM's frame-0 local-index bounds check, so reject it too.
    if (r.err || mod->globalCount < 0 || mod->codeLen < 0 || mod->constCount < 0 || mod->dataCount < 0 || mod->procCount < 0) {
        basModuleFree(mod);
        return NULL;
    }

    // Code
    if (!rOk(&r, mod->codeLen)) {
        basModuleFree(mod);
        return NULL;
    }

    mod->code = (uint8_t *)malloc(mod->codeLen ? mod->codeLen : 1);

    if (!mod->code) {
        basModuleFree(mod);
        return NULL;
    }

    memcpy(mod->code, r.data + r.pos, mod->codeLen);
    r.pos += mod->codeLen;

    // String constant pool
    if (mod->constCount > (r.len - r.pos) / BAS_CONST_MIN_ENTRY) {
        basModuleFree(mod);
        return NULL;
    }

    // calloc(0) is permitted to return NULL on a conforming libc, which would
    // spuriously reject a valid module that simply has no string constants.
    if (mod->constCount > 0) {
        mod->constants = (BasStringT **)calloc(mod->constCount, sizeof(BasStringT *));

        if (!mod->constants) {
            basModuleFree(mod);
            return NULL;
        }

        for (int32_t i = 0; i < mod->constCount; i++) {
            char *s = rStr(&r);
            mod->constants[i] = basStringNew(s ? s : "", s ? (int32_t)strlen(s) : 0);
            free(s);
        }
    }

    // Data pool
    if (mod->dataCount > (r.len - r.pos) / BAS_DATA_MIN_ENTRY) {
        basModuleFree(mod);
        return NULL;
    }

    if (mod->dataCount > 0) {
        mod->dataPool = (BasValueT *)calloc(mod->dataCount, sizeof(BasValueT));

        if (!mod->dataPool) {
            basModuleFree(mod);
            return NULL;
        }

        for (int32_t i = 0; i < mod->dataCount; i++) {
            uint8_t type = rU8(&r);
            mod->dataPool[i].type = type;

            switch (type) {
                case BAS_TYPE_INTEGER:
                    mod->dataPool[i].intVal = (int16_t)rI32(&r);
                    break;

                case BAS_TYPE_LONG:
                    mod->dataPool[i].longVal = rI32(&r);
                    break;

                case BAS_TYPE_SINGLE: {
                    float f;

                    if (rOk(&r, sizeof(float))) {
                        memcpy(&f, r.data + r.pos, sizeof(float));
                        r.pos += sizeof(float);
                    } else {
                        f = 0.0f;
                    }

                    mod->dataPool[i].sngVal = f;
                    break;
                }

                case BAS_TYPE_DOUBLE:
                    mod->dataPool[i].dblVal = rF64(&r);
                    break;

                case BAS_TYPE_STRING: {
                    char *s = rStr(&r);
                    mod->dataPool[i].strVal = basStringNew(s ? s : "", s ? (int32_t)strlen(s) : 0);
                    free(s);
                    break;
                }

                case BAS_TYPE_BOOLEAN:
                    mod->dataPool[i].boolVal = rU8(&r) ? -1 : 0;
                    break;

                default:
                    // A non-scalar type byte means the stream is
                    // desynchronized; reject the whole module.  Coerce
                    // this slot to a release-safe INTEGER first.
                    mod->dataPool[i].type   = BAS_TYPE_INTEGER;
                    mod->dataPool[i].intVal = 0;
                    basModuleFree(mod);
                    return NULL;
            }
        }

        if (r.err) {
            basModuleFree(mod);
            return NULL;
        }
    }

    // Procedure table
    if (mod->procCount > (r.len - r.pos) / BAS_PROC_MIN_ENTRY) {
        basModuleFree(mod);
        return NULL;
    }

    if (mod->procCount > 0) {
        mod->procs = (BasProcEntryT *)calloc(mod->procCount, sizeof(BasProcEntryT));

        if (!mod->procs) {
            basModuleFree(mod);
            return NULL;
        }

        for (int32_t i = 0; i < mod->procCount; i++) {
            BasProcEntryT *p = &mod->procs[i];
            p->codeAddr   = rI32(&r);
            p->paramCount = rI32(&r);
            p->localCount = rI32(&r);
            p->returnType = rU8(&r);
            p->isFunction = rU8(&r) != 0;

            rStrInto(&r, p->name, BAS_MAX_IDENT);
            rStrInto(&r, p->formName, BAS_MAX_IDENT);
        }
    }

    // Form variable info
    mod->formVarInfoCount = readCount(&r, BAS_FORMVAR_MIN_ENTRY);

    if (mod->formVarInfoCount > 0) {
        mod->formVarInfo = (BasFormVarInfoT *)calloc(mod->formVarInfoCount, sizeof(BasFormVarInfoT));

        if (!mod->formVarInfo) {
            basModuleFree(mod);
            return NULL;
        }

        for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
            BasFormVarInfoT *fv = &mod->formVarInfo[i];
            rStrInto(&r, fv->formName, BAS_MAX_IDENT);
            fv->varCount     = rI32(&r);
            fv->initCodeAddr = rI32(&r);
            fv->initCodeLen  = rI32(&r);
        }
    }

    // Global-slot runtime type init table (survives strip).  STRING
    // globals need this so first-use string ops work correctly.
    mod->globalInitCount = readCount(&r, BAS_GLOBALINIT_MIN_ENTRY);

    if (mod->globalInitCount > 0) {
        mod->globalInits = (BasGlobalInitT *)calloc(mod->globalInitCount, sizeof(BasGlobalInitT));

        if (!mod->globalInits) {
            basModuleFree(mod);
            return NULL;
        }

        for (int32_t i = 0; i < mod->globalInitCount; i++) {
            mod->globalInits[i].index    = rI32(&r);
            mod->globalInits[i].dataType = rU8(&r);
        }
    }

    // A latched read error means the stream was truncated or corrupt; the
    // reader returned benign defaults for everything past the fault, so the
    // module would load with zeroed proc/code addresses.  Reject it.
    if (r.err) {
        basModuleFree(mod);
        return NULL;
    }

    return mod;
}


void basModuleFree(BasModuleT *mod) {
    if (!mod) {
        return;
    }

    free(mod->code);

    if (mod->constants) {
        for (int32_t i = 0; i < mod->constCount; i++) {
            basStringUnref(mod->constants[i]);
        }

        free(mod->constants);
    }

    if (mod->dataPool) {
        for (int32_t i = 0; i < mod->dataCount; i++) {
            basValRelease(&mod->dataPool[i]);
        }

        free(mod->dataPool);
    }

    free(mod->procs);
    free(mod->formVarInfo);
    free(mod->globalInits);
    free(mod->debugVars);

    if (mod->debugUdtDefs) {
        for (int32_t i = 0; i < mod->debugUdtDefCount; i++) {
            free(mod->debugUdtDefs[i].fields);
        }

        free(mod->debugUdtDefs);
    }

    free(mod);
}


uint8_t *basModuleSerialize(const BasModuleT *mod, int32_t *outLen) {
    if (!mod || !outLen) {
        return NULL;
    }

    BufT b;
    bufInit(&b);

    // Header
    bufWrite(&b, BAS_MOD_MAGIC, BAS_MOD_MAGIC_LEN);
    bufWriteU16(&b, BAS_MOD_VERSION);
    bufWriteI32(&b, mod->entryPoint);
    bufWriteI32(&b, mod->globalCount);
    bufWriteI32(&b, mod->codeLen);
    bufWriteI32(&b, mod->constCount);
    bufWriteI32(&b, mod->dataCount);
    bufWriteI32(&b, mod->procCount);

    // Code
    bufWrite(&b, mod->code, mod->codeLen);

    // String constant pool
    for (int32_t i = 0; i < mod->constCount; i++) {
        const char *s = mod->constants[i] ? mod->constants[i]->data : "";
        bufWriteStr(&b, s);
    }

    // Data pool
    for (int32_t i = 0; i < mod->dataCount; i++) {
        const BasValueT *v = &mod->dataPool[i];
        bufWriteU8(&b, v->type);

        switch (v->type) {
            case BAS_TYPE_INTEGER:
                bufWriteI32(&b, v->intVal);
                break;

            case BAS_TYPE_LONG:
                bufWriteI32(&b, v->longVal);
                break;

            case BAS_TYPE_SINGLE: {
                float f = v->sngVal;
                bufWrite(&b, &f, sizeof(float));
                break;
            }

            case BAS_TYPE_DOUBLE:
                bufWrite(&b, &v->dblVal, sizeof(double));
                break;

            case BAS_TYPE_STRING:
                bufWriteStr(&b, v->strVal ? v->strVal->data : "");
                break;

            case BAS_TYPE_BOOLEAN:
                bufWriteU8(&b, v->boolVal ? 1 : 0);
                break;

            default:
                bufWriteI32(&b, 0);
                break;
        }
    }

    // Procedure table
    for (int32_t i = 0; i < mod->procCount; i++) {
        const BasProcEntryT *p = &mod->procs[i];
        bufWriteI32(&b, p->codeAddr);
        bufWriteI32(&b, p->paramCount);
        bufWriteI32(&b, p->localCount);
        bufWriteU8(&b, p->returnType);
        bufWriteU8(&b, p->isFunction ? 1 : 0);
        bufWriteStr(&b, p->name);
        bufWriteStr(&b, p->formName);
    }

    // Form variable info (runtime-essential for per-form variable allocation)
    bufWriteI32(&b, mod->formVarInfoCount);

    for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
        const BasFormVarInfoT *fv = &mod->formVarInfo[i];
        bufWriteStr(&b, fv->formName);
        bufWriteI32(&b, fv->varCount);
        bufWriteI32(&b, fv->initCodeAddr);
        bufWriteI32(&b, fv->initCodeLen);
    }

    // Global init table (runtime-essential for STRING default slot type)
    bufWriteI32(&b, mod->globalInitCount);

    for (int32_t i = 0; i < mod->globalInitCount; i++) {
        bufWriteI32(&b, mod->globalInits[i].index);
        bufWriteU8(&b, mod->globalInits[i].dataType);
    }

    if (b.err) {
        free(b.buf);
        return NULL;
    }

    *outLen = b.len;
    return b.buf;
}


static void bufGrow(BufT *b, int32_t need) {
    if (b->err) {
        return;
    }

    if (need < 0 || b->len > INT32_MAX - need) {
        b->err = true;
        return;
    }

    int32_t want = b->len + need;

    // cap can be 0 here, doubling 0 never reaches want, so seed it directly to
    // escape the otherwise-infinite loop.
    if (b->cap < 1) {
        b->cap = want;
    }

    while (b->cap < want) {
        if (b->cap > INT32_MAX / 2) {
            b->cap = want;
            break;
        }

        b->cap *= 2;
    }

    uint8_t *nb = (uint8_t *)realloc(b->buf, b->cap);

    if (!nb) {
        b->err = true;
        return;
    }

    b->buf = nb;
}


static void bufInit(BufT *b) {
    b->cap = BAS_BUF_INIT_CAP;
    b->buf = (uint8_t *)malloc(b->cap);
    b->len = 0;
    b->err = false;

    if (!b->buf) {
        b->cap = 0;
        b->err = true;
    }
}


static void bufWrite(BufT *b, const void *data, int32_t len) {
    bufGrow(b, len);

    if (b->err) {
        return;
    }

    memcpy(b->buf + b->len, data, len);
    b->len += len;
}


static void bufWriteI32(BufT *b, int32_t v) {
    bufWrite(b, &v, sizeof(int32_t));
}


static void bufWriteStr(BufT *b, const char *s) {
    uint16_t len = s ? (uint16_t)strlen(s) : 0;
    bufWriteU16(b, len);

    if (len > 0) {
        bufWrite(b, s, len);
    }
}


static void bufWriteU16(BufT *b, uint16_t v) {
    bufWrite(b, &v, sizeof(uint16_t));
}


static void bufWriteU8(BufT *b, uint8_t v) {
    bufWrite(b, &v, sizeof(uint8_t));
}


// Read an int32 count and validate it is non-negative and that the
// declared minimum per-entry payload still fits in the remaining bytes.
// Uses division, not multiplication, to avoid overflow.
static int32_t readCount(ReaderT *r, int32_t minBytesPerEntry) {
    int32_t count = rI32(r);

    if (r->err || count < 0) {
        r->err = true;
        return 0;
    }

    if (minBytesPerEntry > 0) {
        int32_t remaining = r->len - r->pos;

        if (count > remaining / minBytesPerEntry) {
            r->err = true;
            return 0;
        }
    }

    return count;
}


static double rF64(ReaderT *r) {
    if (!rOk(r, sizeof(double))) {
        return 0.0;
    }

    double v;
    memcpy(&v, r->data + r->pos, sizeof(double));
    r->pos += sizeof(double);
    return v;
}


static int32_t rI32(ReaderT *r) {
    if (!rOk(r, sizeof(int32_t))) {
        return 0;
    }

    int32_t v;
    memcpy(&v, r->data + r->pos, sizeof(int32_t));
    r->pos += sizeof(int32_t);
    return v;
}


static bool rOk(ReaderT *r, int32_t need) {
    // r->len and r->pos are both non-negative and r->pos is within
    // [0, len], so r->len - r->pos cannot overflow; comparing against
    // need this way avoids the overflow of r->pos + need.
    if (need < 0 || r->err || r->pos < 0 || need > r->len - r->pos) {
        r->err = true;
        return false;
    }

    return true;
}


// Read a length-prefixed string into a malloc'd buffer.
static char *rStr(ReaderT *r) {
    uint16_t len = rU16(r);

    if (len == 0 || !rOk(r, len)) {
        char *s = (char *)malloc(1);

        if (!s) {
            r->err = true;
            return NULL;
        }

        s[0] = '\0';
        return s;
    }

    char *s = (char *)malloc(len + 1);

    if (!s) {
        r->err = true;
        return NULL;
    }

    memcpy(s, r->data + r->pos, len);
    s[len] = '\0';
    r->pos += len;
    return s;
}


// Read a length-prefixed string directly into a fixed-size buffer,
// truncating to cap-1 chars.  Wraps rStr so the reader position advances
// by the full on-wire string length regardless of truncation.
static void rStrInto(ReaderT *r, char *dst, int32_t cap) {
    char *s = rStr(r);
    snprintf(dst, cap, "%s", s ? s : "");
    free(s);
}


static uint16_t rU16(ReaderT *r) {
    if (!rOk(r, sizeof(uint16_t))) {
        return 0;
    }

    uint16_t v;
    memcpy(&v, r->data + r->pos, sizeof(uint16_t));
    r->pos += sizeof(uint16_t);
    return v;
}


static uint8_t rU8(ReaderT *r) {
    if (!rOk(r, sizeof(uint8_t))) {
        return 0;
    }

    return r->data[r->pos++];
}
