// squirrelAdapter.c -- Squirrel 3.2 engine adapter for the broker.
//
// One shared trampoline backs every exposed native: the binding (broker + name)
// is the closure's single free variable, which Squirrel pushes onto the stack
// after the call arguments, so the trampoline recovers it from the stack top,
// marshals the arguments to CalogValueT, and dispatches through calogCall (honoring
// the actor route hook). Marshalling covers scalars, binary-safe strings, and the
// hybrid aggregate (Squirrel array <-> list, table <-> map), with the shared depth
// cap guarding cycles.

#define _POSIX_C_SOURCE 200809L

#include "squirrelAdapter.h"

#include <squirrel.h>

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

#define SQUIRREL_BINDING_INITIAL 8
#define SQUIRREL_STACK_SIZE      1024
#define SQUIRREL_MARSHAL_SLOTS   8

// An exposed native is bound by broker + name (not a captured fn pointer) so the
// trampoline dispatches through calogCall and honors the actor route hook: a
// native owned by another context is marshalled to its thread rather than run on
// this VM's thread. With no hook installed, calogCall runs it inline.
typedef struct BindingT {
    CalogT *broker;
    char    *name;
} BindingT;

// Backs a CalogFnT exported from this VM: a pinned (sq_addref'd) closure object
// plus the owning context whose VM must run the invoke and the eventual release.
typedef struct SquirrelExportT {
    CalogSquirrelT *context;
    HSQOBJECT         obj;
} SquirrelExportT;

struct CalogSquirrelT {
    HSQUIRRELVM v;
    CalogT    *broker;
    uint64_t    ctxId;
    BindingT  **bindings;
    int32_t     bindingCount;
    int32_t     bindingCap;
};

static int32_t  squirrelCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void     squirrelCallableRelease(CalogFnT *callable);
static CalogSquirrelT *squirrelContextOf(HSQUIRRELVM v);
static int32_t  squirrelExportAt(CalogSquirrelT *context, SQInteger idx, CalogFnT **out);
static void     squirrelPrint(HSQUIRRELVM v, const SQChar *format, ...);
static int32_t  squirrelPushValueDepth(HSQUIRRELVM v, const CalogValueT *value, int32_t depth);
static int32_t  squirrelToValueDepth(HSQUIRRELVM v, SQInteger idx, CalogValueT *out, int32_t depth);
static SQInteger squirrelTrampoline(HSQUIRRELVM v);


static int32_t squirrelCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    SquirrelExportT *export;
    HSQUIRRELVM      v;
    SQInteger        baseTop;
    int32_t          index;
    int32_t          status;

    export  = (SquirrelExportT *)userData;
    v       = export->context->v;
    calogValueNil(result);
    baseTop = sq_gettop(v);
    if (SQ_FAILED(sq_reservestack(v, SQUIRREL_MARSHAL_SLOTS))) {
        return calogFail(result, calogErrOomE, "out of stack invoking Squirrel closure");
    }
    sq_pushobject(v, export->obj);
    sq_pushroottable(v);
    for (index = 0; index < argCount; index++) {
        status = squirrelPushValueDepth(v, &args[index], 0);
        if (status != calogOkE) {
            sq_settop(v, baseTop);
            return calogFail(result, status, "failed to marshal argument into Squirrel");
        }
    }
    if (SQ_FAILED(sq_call(v, (SQInteger)(argCount + 1), SQTrue, SQTrue))) {
        sq_settop(v, baseTop);
        return calogFail(result, calogErrArgE, "Squirrel closure call failed");
    }
    status = squirrelToValueDepth(v, -1, result, 0);
    sq_settop(v, baseTop);
    return status;
}


static void squirrelCallableRelease(CalogFnT *callable) {
    SquirrelExportT *export;

    export = (SquirrelExportT *)calogFnUserData(callable);
    sq_release(export->context->v, &export->obj);
    free(export);
}


int32_t calogSquirrelCreate(CalogSquirrelT **out, CalogT *broker, uint64_t ctxId) {
    CalogSquirrelT *context;
    HSQUIRRELVM       v;

    *out = NULL;
    context = (CalogSquirrelT *)calloc(1, sizeof(*context));
    if (context == NULL) {
        return calogErrOomE;
    }
    v = sq_open(SQUIRREL_STACK_SIZE);
    if (v == NULL) {
        free(context);
        return calogErrOomE;
    }
    context->v      = v;
    context->broker = broker;
    context->ctxId  = ctxId;
    sq_setforeignptr(v, context);
    sq_setprintfunc(v, squirrelPrint, squirrelPrint);
    *out = context;
    return calogOkE;
}


void calogSquirrelDestroy(CalogSquirrelT *context) {
    int32_t index;

    if (context == NULL) {
        return;
    }
    if (context->v != NULL) {
        sq_close(context->v);
    }
    for (index = 0; index < context->bindingCount; index++) {
        free(context->bindings[index]->name);
        free(context->bindings[index]);
    }
    free(context->bindings);
    free(context);
}


static CalogSquirrelT *squirrelContextOf(HSQUIRRELVM v) {
    return (CalogSquirrelT *)sq_getforeignptr(v);
}


int32_t calogSquirrelExport(CalogSquirrelT *context, const char *globalName, CalogFnT **out) {
    HSQUIRRELVM  v;
    SQObjectType type;
    SQInteger    baseTop;
    int32_t      status;

    v       = context->v;
    *out    = NULL;
    baseTop = sq_gettop(v);
    sq_pushroottable(v);
    sq_pushstring(v, globalName, -1);
    if (SQ_FAILED(sq_get(v, -2))) {
        sq_settop(v, baseTop);
        return calogErrNotFoundE;
    }
    type = sq_gettype(v, -1);
    if (type != OT_CLOSURE && type != OT_NATIVECLOSURE) {
        sq_settop(v, baseTop);
        return calogErrTypeE;
    }
    status = squirrelExportAt(context, -1, out);
    sq_settop(v, baseTop);
    return status;
}


static int32_t squirrelExportAt(CalogSquirrelT *context, SQInteger idx, CalogFnT **out) {
    HSQUIRRELVM      v;
    SquirrelExportT *export;
    HSQOBJECT        obj;
    int32_t          status;

    v      = context->v;
    *out   = NULL;
    export = (SquirrelExportT *)malloc(sizeof(*export));
    if (export == NULL) {
        return calogErrOomE;
    }
    sq_resetobject(&obj);
    if (SQ_FAILED(sq_getstackobj(v, idx, &obj))) {
        free(export);
        return calogErrTypeE;
    }
    sq_addref(v, &obj);
    export->context = context;
    export->obj     = obj;
    status = calogFnCreate(out, context->broker, squirrelCallableInvoke, export, squirrelCallableRelease, context->ctxId);
    if (status != calogOkE) {
        sq_release(v, &obj);
        free(export);
        return status;
    }
    return calogOkE;
}


int32_t calogSquirrelExpose(CalogSquirrelT *context, const char *name) {
    CalogEntryT *entry;
    BindingT     *binding;
    HSQUIRRELVM   v;

    v = context->v;
    entry = calogLookup(context->broker, name);
    if (entry == NULL) {
        return calogErrNotFoundE;
    }
    binding = (BindingT *)malloc(sizeof(*binding));
    if (binding == NULL) {
        return calogErrOomE;
    }
    binding->broker = context->broker;
    binding->name   = strdup(name);
    if (binding->name == NULL) {
        free(binding);
        return calogErrOomE;
    }
    if (context->bindingCount == context->bindingCap) {
        int32_t    newCap;
        BindingT **resized;
        newCap  = (context->bindingCap == 0) ? SQUIRREL_BINDING_INITIAL : context->bindingCap * CALOG_GROWTH_FACTOR;
        resized = (BindingT **)realloc(context->bindings, (size_t)newCap * sizeof(BindingT *));
        if (resized == NULL) {
            free(binding->name);
            free(binding);
            return calogErrOomE;
        }
        context->bindings   = resized;
        context->bindingCap = newCap;
    }
    context->bindings[context->bindingCount] = binding;
    context->bindingCount++;

    sq_pushroottable(v);
    sq_pushstring(v, name, -1);
    sq_pushuserpointer(v, binding);
    sq_newclosure(v, squirrelTrampoline, 1);
    sq_newslot(v, -3, SQFalse);
    sq_pop(v, 1);
    return calogOkE;
}


static void squirrelPrint(HSQUIRRELVM v, const SQChar *format, ...) {
    va_list args;

    (void)v;
    va_start(args, format);
    vfprintf(stdout, format, args);
    va_end(args);
}


static int32_t squirrelPushValueDepth(HSQUIRRELVM v, const CalogValueT *value, int32_t depth) {
    if (depth > CALOG_MAX_DEPTH) {
        return calogErrDepthE;
    }
    // The VM stack does not auto-grow on push (only EnterFrame resizes it), so
    // reserve headroom for this level's pushes before recursing -- otherwise a
    // deep marshal atop an already-deep script stack could overflow it.
    if (SQ_FAILED(sq_reservestack(v, SQUIRREL_MARSHAL_SLOTS))) {
        return calogErrOomE;
    }
    switch (value->type) {
        case calogNilE:
            sq_pushnull(v);
            return calogOkE;
        case calogBoolE:
            sq_pushbool(v, value->as.b ? SQTrue : SQFalse);
            return calogOkE;
        case calogIntE:
            sq_pushinteger(v, (SQInteger)value->as.i);
            return calogOkE;
        case calogRealE:
            sq_pushfloat(v, (SQFloat)value->as.r);
            return calogOkE;
        case calogStringE:
            sq_pushstring(v, value->as.s.bytes, (SQInteger)value->as.s.length);
            return calogOkE;
        case calogAggE: {
            CalogAggT *aggregate;
            SQInteger   baseTop;
            int32_t     status;
            int64_t     index;
            bool        asTable;
            aggregate = value->as.agg;
            baseTop   = sq_gettop(v);
            // A Squirrel array holds only a sequence; anything with keyed entries
            // (or an explicit map) must become a table.
            asTable = aggregate->pairCount > 0 || aggregate->kind == calogMapE;
            if (!asTable) {
                sq_newarray(v, 0);
                for (index = 0; index < aggregate->arrayCount; index++) {
                    status = squirrelPushValueDepth(v, &aggregate->array[index], depth + 1);
                    if (status != calogOkE) {
                        sq_settop(v, baseTop);
                        return status;
                    }
                    if (SQ_FAILED(sq_arrayappend(v, -2))) {
                        sq_settop(v, baseTop);
                        return calogErrUnsupportedE;
                    }
                }
                return calogOkE;
            }
            sq_newtable(v);
            for (index = 0; index < aggregate->arrayCount; index++) {
                sq_pushinteger(v, (SQInteger)index);
                status = squirrelPushValueDepth(v, &aggregate->array[index], depth + 1);
                if (status != calogOkE) {
                    sq_settop(v, baseTop);
                    return status;
                }
                if (SQ_FAILED(sq_newslot(v, -3, SQFalse))) {
                    sq_settop(v, baseTop);
                    return calogErrUnsupportedE;
                }
            }
            for (index = 0; index < aggregate->pairCount; index++) {
                status = squirrelPushValueDepth(v, &aggregate->pairs[index].key, depth + 1);
                if (status != calogOkE) {
                    sq_settop(v, baseTop);
                    return status;
                }
                status = squirrelPushValueDepth(v, &aggregate->pairs[index].value, depth + 1);
                if (status != calogOkE) {
                    sq_settop(v, baseTop);
                    return status;
                }
                if (SQ_FAILED(sq_newslot(v, -3, SQFalse))) {
                    sq_settop(v, baseTop);
                    return calogErrUnsupportedE;
                }
            }
            return calogOkE;
        }
        case calogFnE:
            // Pushing a foreign function value INTO Squirrel is the remaining v1
            // limit (export OUT is supported via calogSquirrelExport); see the header.
            return calogErrUnsupportedE;
    }
    return calogErrTypeE;
}


int32_t calogSquirrelRun(CalogSquirrelT *context, const char *source) {
    HSQUIRRELVM v;
    SQInteger   baseTop;

    v       = context->v;
    baseTop = sq_gettop(v);
    if (SQ_FAILED(sq_compilebuffer(v, source, (SQInteger)strlen(source), _SC("calog"), SQTrue))) {
        fprintf(stderr, "squirrel compile error\n");
        sq_settop(v, baseTop);
        return calogErrArgE;
    }
    sq_pushroottable(v);
    if (SQ_FAILED(sq_call(v, 1, SQFalse, SQTrue))) {
        fprintf(stderr, "squirrel run error\n");
        sq_settop(v, baseTop);
        return calogErrArgE;
    }
    sq_settop(v, baseTop);
    return calogOkE;
}


static int32_t squirrelToValueDepth(HSQUIRRELVM v, SQInteger idx, CalogValueT *out, int32_t depth) {
    SQObjectType type;
    SQInteger    absIdx;

    calogValueNil(out);
    if (depth > CALOG_MAX_DEPTH) {
        return calogErrDepthE;
    }
    absIdx = (idx < 0) ? sq_gettop(v) + idx + 1 : idx;
    type   = sq_gettype(v, absIdx);
    switch (type) {
        case OT_NULL:
            return calogOkE;
        case OT_BOOL: {
            SQBool b;
            sq_getbool(v, absIdx, &b);
            calogValueBool(out, b != SQFalse);
            return calogOkE;
        }
        case OT_INTEGER: {
            SQInteger i;
            sq_getinteger(v, absIdx, &i);
            calogValueInt(out, (int64_t)i);
            return calogOkE;
        }
        case OT_FLOAT: {
            SQFloat f;
            sq_getfloat(v, absIdx, &f);
            calogValueReal(out, (double)f);
            return calogOkE;
        }
        case OT_STRING: {
            const SQChar *bytes;
            SQInteger     len;
            sq_getstringandsize(v, absIdx, &bytes, &len);
            return calogValueString(out, bytes, (int64_t)len);
        }
        case OT_ARRAY:
        case OT_TABLE: {
            CalogAggT *aggregate;
            SQInteger   baseTop;
            int32_t     status;
            bool        isMap;
            isMap  = (type == OT_TABLE);
            status = calogAggCreate(&aggregate, isMap ? calogMapE : calogListE);
            if (status != calogOkE) {
                return status;
            }
            if (SQ_FAILED(sq_reservestack(v, SQUIRREL_MARSHAL_SLOTS))) {
                calogAggFree(aggregate);
                return calogErrOomE;
            }
            baseTop = sq_gettop(v);
            sq_pushnull(v);
            while (SQ_SUCCEEDED(sq_next(v, absIdx))) {
                CalogValueT element;
                // sq_next leaves key at -2 and value at -1 above the iterator.
                status = squirrelToValueDepth(v, -1, &element, depth + 1);
                if (status != calogOkE) {
                    sq_settop(v, baseTop);
                    calogAggFree(aggregate);
                    return status;
                }
                if (isMap) {
                    CalogValueT keyValue;
                    status = squirrelToValueDepth(v, -2, &keyValue, depth + 1);
                    if (status != calogOkE) {
                        calogValueFree(&element);
                        sq_settop(v, baseTop);
                        calogAggFree(aggregate);
                        return status;
                    }
                    status = calogAggSet(aggregate, &keyValue, &element);
                    if (status != calogOkE) {
                        calogValueFree(&keyValue);
                        calogValueFree(&element);
                        sq_settop(v, baseTop);
                        calogAggFree(aggregate);
                        return status;
                    }
                } else {
                    status = calogAggPush(aggregate, &element);
                    if (status != calogOkE) {
                        calogValueFree(&element);
                        sq_settop(v, baseTop);
                        calogAggFree(aggregate);
                        return status;
                    }
                }
                sq_pop(v, 2);
            }
            sq_settop(v, baseTop);
            calogValueAgg(out, aggregate);
            return calogOkE;
        }
        case OT_CLOSURE:
        case OT_NATIVECLOSURE: {
            CalogFnT *callable;
            int32_t    status;
            // A closure crossing out becomes a refcounted CalogFnT over the pinned
            // object; the owning context is recovered from the VM's foreign pointer.
            status = squirrelExportAt(squirrelContextOf(v), absIdx, &callable);
            if (status != calogOkE) {
                return status;
            }
            calogValueFn(out, callable);
            return calogOkE;
        }
        default:
            return calogErrUnsupportedE;
    }
}


static SQInteger squirrelTrampoline(HSQUIRRELVM v) {
    BindingT     *binding;
    SQUserPointer ptr;
    CalogValueT       *args;
    CalogValueT        result;
    SQInteger     top;
    int32_t       argCount;
    int32_t       index;
    int32_t       status;

    // The binding pointer is the closure's single free variable, which the VM
    // pushes onto the stack after the call arguments -- so it sits at the top.
    // 'this' is at index 1, the arguments at 2..top-1.
    top = sq_gettop(v);
    sq_getuserpointer(v, top, &ptr);
    binding  = (BindingT *)ptr;
    argCount = (int32_t)(top - 2);
    if (argCount < 0) {
        argCount = 0;
    }
    args = NULL;
    if (argCount > 0) {
        args = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT));
        if (args == NULL) {
            return sq_throwerror(v, _SC("out of memory marshalling native args"));
        }
    }
    for (index = 0; index < argCount; index++) {
        status = squirrelToValueDepth(v, index + 2, &args[index], 0);
        if (status != calogOkE) {
            int32_t cleanup;
            for (cleanup = 0; cleanup < index; cleanup++) {
                calogValueFree(&args[cleanup]);
            }
            free(args);
            return sq_throwerror(v, _SC("failed to marshal native argument"));
        }
    }
    status = calogCall(binding->broker, binding->name, args, argCount, &result);
    for (index = 0; index < argCount; index++) {
        calogValueFree(&args[index]);
    }
    free(args);
    if (status != calogOkE) {
        const SQChar *message;
        message = (result.type == calogStringE) ? result.as.s.bytes : _SC("native call failed");
        sq_throwerror(v, message);
        calogValueFree(&result);
        return SQ_ERROR;
    }
    status = squirrelPushValueDepth(v, &result, 0);
    calogValueFree(&result);
    if (status != calogOkE) {
        return sq_throwerror(v, _SC("failed to marshal native result"));
    }
    return 1;
}
