// jsAdapter.c -- Duktape (JavaScript) engine adapter for the broker.
//
// One shared trampoline backs every exposed native: the binding (broker + name) is
// a hidden property on the function object, recovered via duk_push_current_function,
// so the trampoline marshals the JS arguments to CalogValueT and dispatches through
// calogCall (honoring the actor route hook). Marshalling covers scalars, binary-
// safe strings, and the hybrid aggregate (array <-> list, object <-> map), with the
// shared depth cap guarding cycles. A JS function crossing out becomes a refcounted
// CalogFnT over a heap pointer kept alive by a per-heap export registry.
//
// Duktape error model: on an internal failure the trampoline / callable raise a
// JS error via duk_error (which longjmps), after releasing any CalogValueT it owns.

#define _POSIX_C_SOURCE 200809L

#include "jsAdapter.h"

#include "duktape.h"

#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define JS_BINDING_INITIAL 8
#define JS_ERR_MSG_CAP     256

// Hidden/internal property keys (the leading 0xFF byte makes them non-enumerable
// and unreachable from script). JS_BINDING_KEY lives on each native function;
// JS_CONTEXT_KEY and JS_EXPORTS_KEY live in the global stash.
#define JS_BINDING_KEY "\xFF" "calogBinding"
#define JS_CONTEXT_KEY "\xFF" "calogContext"
#define JS_EXPORTS_KEY "\xFF" "calogExports"

// 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
// heap's thread. With no hook installed, calogCall runs it inline.
typedef struct BindingT {
    CalogT *broker;
    char    *name;
} BindingT;

// Backs a CalogFnT exported from this heap: the function's heap pointer (for fast
// invocation) and its slot index in the export registry (which keeps it reachable;
// dropped on release), plus the owning context whose heap runs both.
typedef struct JsExportT {
    CalogJsT *context;
    void       *heapPtr;
    int32_t     ref;
} JsExportT;

struct CalogJsT {
    duk_context *ctx;
    CalogT     *broker;
    uint64_t     ctxId;
    BindingT   **bindings;
    int32_t      bindingCount;
    int32_t      bindingCap;
    int32_t      nextRef;
};

static int32_t     jsCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void        jsCallableRelease(CalogFnT *callable);
static CalogJsT *jsContextOf(duk_context *ctx);
static int32_t     jsExportAt(CalogJsT *context, duk_idx_t idx, CalogFnT **out);
static int32_t     jsPushValueDepth(duk_context *ctx, const CalogValueT *value, int32_t depth);
static int32_t     jsToValueDepth(duk_context *ctx, duk_idx_t idx, CalogValueT *out, int32_t depth);
static duk_ret_t   jsTrampoline(duk_context *ctx);


static int32_t jsCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    JsExportT   *export;
    duk_context *ctx;
    duk_idx_t    baseTop;
    int32_t      index;
    int32_t      status;

    export = (JsExportT *)userData;
    ctx    = export->context->ctx;
    calogValueNil(result);
    baseTop = duk_get_top(ctx);
    duk_push_heapptr(ctx, export->heapPtr);
    for (index = 0; index < argCount; index++) {
        status = jsPushValueDepth(ctx, &args[index], 0);
        if (status != calogOkE) {
            duk_set_top(ctx, baseTop);
            return calogFail(result, status, "failed to marshal argument into JavaScript");
        }
    }
    if (duk_pcall(ctx, argCount) != 0) {
        duk_size_t  len;
        const char *message;
        message = duk_safe_to_lstring(ctx, -1, &len);
        status  = calogFail(result, calogErrArgE, message != NULL ? message : "JavaScript call failed");
        duk_set_top(ctx, baseTop);
        return status;
    }
    status = jsToValueDepth(ctx, -1, result, 0);
    duk_set_top(ctx, baseTop);
    return status;
}


static void jsCallableRelease(CalogFnT *callable) {
    JsExportT   *export;
    duk_context *ctx;

    export = (JsExportT *)calogFnUserData(callable);
    ctx    = export->context->ctx;
    // Drop the registry slot so the pinned function becomes collectable.
    duk_push_global_stash(ctx);
    duk_get_prop_string(ctx, -1, JS_EXPORTS_KEY);
    duk_del_prop_index(ctx, -1, (duk_uarridx_t)export->ref);
    duk_pop_2(ctx);
    free(export);
}


int32_t calogJsCreate(CalogJsT **out, CalogT *broker, uint64_t ctxId) {
    CalogJsT  *context;
    duk_context *ctx;

    *out = NULL;
    context = (CalogJsT *)calloc(1, sizeof(*context));
    if (context == NULL) {
        return calogErrOomE;
    }
    ctx = duk_create_heap_default();
    if (ctx == NULL) {
        free(context);
        return calogErrOomE;
    }
    context->ctx    = ctx;
    context->broker = broker;
    context->ctxId  = ctxId;

    // Stash the context pointer (so the marshaller can reach it) and an empty export
    // registry object (which pins exported functions against GC).
    duk_push_global_stash(ctx);
    duk_push_pointer(ctx, context);
    duk_put_prop_string(ctx, -2, JS_CONTEXT_KEY);
    duk_push_object(ctx);
    duk_put_prop_string(ctx, -2, JS_EXPORTS_KEY);
    duk_pop(ctx);

    *out = context;
    return calogOkE;
}


void calogJsDestroy(CalogJsT *context) {
    int32_t index;

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


static CalogJsT *jsContextOf(duk_context *ctx) {
    CalogJsT *context;

    duk_push_global_stash(ctx);
    duk_get_prop_string(ctx, -1, JS_CONTEXT_KEY);
    context = (CalogJsT *)duk_get_pointer(ctx, -1);
    duk_pop_2(ctx);
    return context;
}


int32_t calogJsExport(CalogJsT *context, const char *globalName, CalogFnT **out) {
    duk_context *ctx;
    duk_idx_t    baseTop;
    int32_t      status;

    ctx     = context->ctx;
    *out    = NULL;
    baseTop = duk_get_top(ctx);
    if (!duk_get_global_string(ctx, globalName)) {
        duk_set_top(ctx, baseTop);
        return calogErrNotFoundE;
    }
    if (!duk_is_function(ctx, -1)) {
        duk_set_top(ctx, baseTop);
        return calogErrTypeE;
    }
    status = jsExportAt(context, -1, out);
    duk_set_top(ctx, baseTop);
    return status;
}


static int32_t jsExportAt(CalogJsT *context, duk_idx_t idx, CalogFnT **out) {
    duk_context *ctx;
    JsExportT   *export;
    void        *heapPtr;
    int32_t      ref;
    int32_t      status;

    ctx  = context->ctx;
    *out = NULL;
    if (idx < 0) {
        idx = duk_get_top(ctx) + idx;
    }
    export = (JsExportT *)malloc(sizeof(*export));
    if (export == NULL) {
        return calogErrOomE;
    }
    // Pin the function in the export registry (keeps it GC-reachable) and remember
    // its heap pointer for fast invocation.
    duk_push_global_stash(ctx);
    duk_get_prop_string(ctx, -1, JS_EXPORTS_KEY);
    duk_dup(ctx, idx);
    heapPtr = duk_get_heapptr(ctx, -1);
    ref     = context->nextRef;
    duk_put_prop_index(ctx, -2, (duk_uarridx_t)ref);
    duk_pop_2(ctx);
    context->nextRef++;

    export->context = context;
    export->heapPtr = heapPtr;
    export->ref     = ref;
    status = calogFnCreate(out, context->broker, jsCallableInvoke, export, jsCallableRelease, context->ctxId);
    if (status != calogOkE) {
        duk_push_global_stash(ctx);
        duk_get_prop_string(ctx, -1, JS_EXPORTS_KEY);
        duk_del_prop_index(ctx, -1, (duk_uarridx_t)ref);
        duk_pop_2(ctx);
        free(export);
        return status;
    }
    return calogOkE;
}


int32_t calogJsExpose(CalogJsT *context, const char *name) {
    CalogEntryT *entry;
    BindingT     *binding;
    duk_context  *ctx;

    ctx   = context->ctx;
    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) ? JS_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++;

    // global[name] = function with the binding pointer as a hidden property.
    duk_push_c_function(ctx, jsTrampoline, DUK_VARARGS);
    duk_push_pointer(ctx, binding);
    duk_put_prop_string(ctx, -2, JS_BINDING_KEY);
    duk_put_global_string(ctx, name);
    return calogOkE;
}


static int32_t jsPushValueDepth(duk_context *ctx, const CalogValueT *value, int32_t depth) {
    if (depth > CALOG_MAX_DEPTH) {
        return calogErrDepthE;
    }
    switch (value->type) {
        case calogNilE:
            duk_push_null(ctx);
            return calogOkE;
        case calogBoolE:
            duk_push_boolean(ctx, value->as.b ? 1 : 0);
            return calogOkE;
        case calogIntE:
            // JS numbers are doubles; magnitudes above 2^53 lose precision.
            duk_push_number(ctx, (double)value->as.i);
            return calogOkE;
        case calogRealE:
            duk_push_number(ctx, value->as.r);
            return calogOkE;
        case calogStringE:
            duk_push_lstring(ctx, value->as.s.bytes, (duk_size_t)value->as.s.length);
            return calogOkE;
        case calogAggE: {
            CalogAggT *aggregate;
            duk_idx_t   objIdx;
            int32_t     status;
            int64_t     index;
            bool        asObject;
            aggregate = value->as.agg;
            // A JS array holds only a sequence; anything keyed (or an explicit map)
            // becomes an object.
            asObject = aggregate->pairCount > 0 || aggregate->kind == calogMapE;
            if (!asObject) {
                objIdx = duk_push_array(ctx);
                for (index = 0; index < aggregate->arrayCount; index++) {
                    status = jsPushValueDepth(ctx, &aggregate->array[index], depth + 1);
                    if (status != calogOkE) {
                        duk_pop(ctx);
                        return status;
                    }
                    duk_put_prop_index(ctx, objIdx, (duk_uarridx_t)index);
                }
                return calogOkE;
            }
            objIdx = duk_push_object(ctx);
            for (index = 0; index < aggregate->arrayCount; index++) {
                status = jsPushValueDepth(ctx, &aggregate->array[index], depth + 1);
                if (status != calogOkE) {
                    duk_pop(ctx);
                    return status;
                }
                duk_put_prop_index(ctx, objIdx, (duk_uarridx_t)index);
            }
            for (index = 0; index < aggregate->pairCount; index++) {
                const CalogValueT *key;
                // JS object keys are strings; coerce a scalar key to its string form,
                // skip a non-stringable (aggregate/function) key.
                key = &aggregate->pairs[index].key;
                status = jsPushValueDepth(ctx, &aggregate->pairs[index].value, depth + 1);
                if (status != calogOkE) {
                    duk_pop(ctx);
                    return status;
                }
                if (key->type == calogStringE) {
                    duk_put_prop_lstring(ctx, objIdx, key->as.s.bytes, (duk_size_t)key->as.s.length);
                } else if (key->type == calogIntE) {
                    duk_put_prop_index(ctx, objIdx, (duk_uarridx_t)key->as.i);
                } else {
                    duk_pop(ctx);
                }
            }
            return calogOkE;
        }
        case calogFnE:
            // Pushing a foreign function value INTO JavaScript is a v1 fidelity limit
            // (export OUT is supported via calogJsExport); see the remaining-limit note.
            return calogErrUnsupportedE;
    }
    return calogErrTypeE;
}


int32_t calogJsRun(CalogJsT *context, const char *source) {
    duk_context *ctx;
    duk_idx_t    baseTop;

    ctx     = context->ctx;
    baseTop = duk_get_top(ctx);
    if (duk_peval_lstring(ctx, source, (duk_size_t)strlen(source)) != 0) {
        fprintf(stderr, "js eval error: %s\n", duk_safe_to_string(ctx, -1));
        duk_set_top(ctx, baseTop);
        return calogErrArgE;
    }
    duk_set_top(ctx, baseTop);
    return calogOkE;
}


static int32_t jsToValueDepth(duk_context *ctx, duk_idx_t idx, CalogValueT *out, int32_t depth) {
    calogValueNil(out);
    if (depth > CALOG_MAX_DEPTH) {
        return calogErrDepthE;
    }
    if (idx < 0) {
        idx = duk_get_top(ctx) + idx;
    }
    switch (duk_get_type(ctx, idx)) {
        case DUK_TYPE_UNDEFINED:
        case DUK_TYPE_NULL:
            return calogOkE;
        case DUK_TYPE_BOOLEAN:
            calogValueBool(out, duk_get_boolean(ctx, idx) != 0);
            return calogOkE;
        case DUK_TYPE_NUMBER: {
            double number;
            number = duk_get_number(ctx, idx);
            // An integral value in int64 range round-trips as an integer; otherwise a
            // real (matching the cross-engine fidelity of Lua's number split).
            if (isfinite(number) && number == floor(number) && number >= -9223372036854775808.0 && number < 9223372036854775808.0) {
                calogValueInt(out, (int64_t)number);
            } else {
                calogValueReal(out, number);
            }
            return calogOkE;
        }
        case DUK_TYPE_STRING: {
            duk_size_t  len;
            const char *bytes;
            bytes = duk_get_lstring(ctx, idx, &len);
            return calogValueString(out, bytes, (int64_t)len);
        }
        case DUK_TYPE_OBJECT: {
            if (duk_is_function(ctx, idx)) {
                CalogFnT *callable;
                int32_t    status;
                status = jsExportAt(jsContextOf(ctx), idx, &callable);
                if (status != calogOkE) {
                    return status;
                }
                calogValueFn(out, callable);
                return calogOkE;
            }
            if (duk_is_array(ctx, idx)) {
                CalogAggT *aggregate;
                duk_size_t  length;
                duk_size_t  i;
                int32_t     status;
                status = calogAggCreate(&aggregate, calogListE);
                if (status != calogOkE) {
                    return status;
                }
                length = duk_get_length(ctx, idx);
                for (i = 0; i < length; i++) {
                    CalogValueT element;
                    duk_get_prop_index(ctx, idx, (duk_uarridx_t)i);
                    status = jsToValueDepth(ctx, -1, &element, depth + 1);
                    duk_pop(ctx);
                    if (status != calogOkE) {
                        calogAggFree(aggregate);
                        return status;
                    }
                    status = calogAggPush(aggregate, &element);
                    if (status != calogOkE) {
                        calogValueFree(&element);
                        calogAggFree(aggregate);
                        return status;
                    }
                }
                calogValueAgg(out, aggregate);
                return calogOkE;
            }
            // Plain object -> map. Enumerate own enumerable string keys.
            {
                CalogAggT *aggregate;
                duk_idx_t   baseTop;
                int32_t     status;
                status = calogAggCreate(&aggregate, calogMapE);
                if (status != calogOkE) {
                    return status;
                }
                baseTop = duk_get_top(ctx);
                duk_enum(ctx, idx, DUK_ENUM_OWN_PROPERTIES_ONLY);
                while (duk_next(ctx, -1, 1)) {
                    CalogValueT keyValue;
                    CalogValueT valValue;
                    // key at -2, value at -1.
                    status = jsToValueDepth(ctx, -2, &keyValue, depth + 1);
                    if (status != calogOkE) {
                        duk_set_top(ctx, baseTop);
                        calogAggFree(aggregate);
                        return status;
                    }
                    status = jsToValueDepth(ctx, -1, &valValue, depth + 1);
                    if (status != calogOkE) {
                        calogValueFree(&keyValue);
                        duk_set_top(ctx, baseTop);
                        calogAggFree(aggregate);
                        return status;
                    }
                    status = calogAggSet(aggregate, &keyValue, &valValue);
                    if (status != calogOkE) {
                        calogValueFree(&keyValue);
                        calogValueFree(&valValue);
                        duk_set_top(ctx, baseTop);
                        calogAggFree(aggregate);
                        return status;
                    }
                    duk_pop_2(ctx);
                }
                duk_set_top(ctx, baseTop);
                calogValueAgg(out, aggregate);
                return calogOkE;
            }
        }
        default:
            return calogErrUnsupportedE;
    }
}


static duk_ret_t jsTrampoline(duk_context *ctx) {
    BindingT *binding;
    CalogValueT   *args;
    CalogValueT    result;
    duk_idx_t top;
    int32_t   argCount;
    int32_t   index;
    int32_t   status;
    char      message[JS_ERR_MSG_CAP];

    duk_push_current_function(ctx);
    duk_get_prop_string(ctx, -1, JS_BINDING_KEY);
    binding = (BindingT *)duk_get_pointer(ctx, -1);
    duk_pop_2(ctx);

    top      = duk_get_top(ctx);
    argCount = (int32_t)top;
    args     = NULL;
    if (argCount > 0) {
        args = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT));
        if (args == NULL) {
            return duk_error(ctx, DUK_ERR_ERROR, "out of memory marshalling native args");
        }
    }
    for (index = 0; index < argCount; index++) {
        status = jsToValueDepth(ctx, index, &args[index], 0);
        if (status != calogOkE) {
            int32_t cleanup;
            for (cleanup = 0; cleanup < index; cleanup++) {
                calogValueFree(&args[cleanup]);
            }
            free(args);
            return duk_error(ctx, DUK_ERR_ERROR, "failed to marshal native argument %d", index + 1);
        }
    }
    status = calogCall(binding->broker, binding->name, args, argCount, &result);
    for (index = 0; index < argCount; index++) {
        calogValueFree(&args[index]);
    }
    free(args);
    if (status != calogOkE) {
        // Copy the message out before freeing result; duk_error longjmps, so nothing
        // after it runs.
        snprintf(message, sizeof(message), "%s", (result.type == calogStringE) ? result.as.s.bytes : "native call failed");
        calogValueFree(&result);
        return duk_error(ctx, DUK_ERR_ERROR, "%s", message);
    }
    status = jsPushValueDepth(ctx, &result, 0);
    calogValueFree(&result);
    if (status != calogOkE) {
        return duk_error(ctx, DUK_ERR_ERROR, "failed to marshal native result");
    }
    return 1;
}
