// luaAdapter.c -- Lua 5.4 engine adapter for the broker.
//
// The single trampoline recovers a native binding from a light-userdata upvalue
// and marshals the Lua call stack to/from CalogValueT. The owning CalogLuaT is
// stashed in the state's extraspace so any marshaller can reach it. A Lua
// function crossing the boundary becomes a CalogFnT backed by a luaL_ref; a
// CalogFnT crossing into Lua becomes a callable userdata whose __gc drops the
// reference.

#define _POSIX_C_SOURCE 200809L

#include "luaAdapter.h"

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define LUA_BINDING_INITIAL 8
#define LUA_FUNCTION_META   "calogFunctionValue"
#define LUA_MARSHAL_SLOTS   8

// An exposed native is bound by broker + name, not by 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 instead of being
// run (wrongly) on this state's thread. With no hook installed, calogCall runs
// it inline -- identical to the single-threaded path.
typedef struct BindingT {
    CalogT *broker;
    char    *name;
} BindingT;

typedef struct LuaExportT {
    CalogLuaT *context;
    int32_t      ref;
} LuaExportT;

struct CalogLuaT {
    lua_State *L;
    CalogT   *broker;
    uint64_t   ctxId;
    BindingT **bindings;
    int32_t    bindingCount;
    int32_t    bindingCap;
};

static int32_t      luaCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void         luaCallableRelease(CalogFnT *callable);
static CalogLuaT *luaContextOf(lua_State *L);
static int32_t      luaExportAt(CalogLuaT *context, int idx, CalogFnT **out);
static int          luaFunctionCall(lua_State *L);
static int          luaFunctionGc(lua_State *L);
static int32_t      luaPushValueDepth(lua_State *L, const CalogValueT *value, int32_t depth);
static int32_t      luaToValueDepth(lua_State *L, int idx, CalogValueT *out, int32_t depth);
static int          luaTrampoline(lua_State *L);


static int32_t luaCallableInvoke(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
    LuaExportT *export;
    lua_State  *L;
    int32_t     index;
    int32_t     status;
    int         baseTop;

    export = (LuaExportT *)userData;
    L      = export->context->L;
    calogValueNil(result);
    baseTop = lua_gettop(L);
    lua_rawgeti(L, LUA_REGISTRYINDEX, export->ref);
    if (lua_type(L, -1) != LUA_TFUNCTION) {
        lua_settop(L, baseTop);
        return calogFail(result, calogErrTypeE, "exported value is not a function");
    }
    for (index = 0; index < argCount; index++) {
        status = luaPushValueDepth(L, &args[index], 0);
        if (status != calogOkE) {
            lua_settop(L, baseTop);
            return calogFail(result, status, "failed to marshal argument into Lua");
        }
    }
    if (lua_pcall(L, argCount, 1, 0) != LUA_OK) {
        size_t      len;
        const char *message;
        message = lua_tolstring(L, -1, &len);
        status = calogFail(result, calogErrArgE, message != NULL ? message : "Lua call failed");
        lua_settop(L, baseTop);
        return status;
    }
    status = luaToValueDepth(L, -1, result, 0);
    lua_settop(L, baseTop);
    return status;
}


static void luaCallableRelease(CalogFnT *callable) {
    LuaExportT *export;

    export = (LuaExportT *)calogFnUserData(callable);
    luaL_unref(export->context->L, LUA_REGISTRYINDEX, export->ref);
    free(export);
}


int32_t calogLuaCreate(CalogLuaT **out, CalogT *broker, uint64_t ctxId) {
    CalogLuaT *context;
    lua_State   *L;

    *out = NULL;
    context = (CalogLuaT *)calloc(1, sizeof(*context));
    if (context == NULL) {
        return calogErrOomE;
    }
    L = luaL_newstate();
    if (L == NULL) {
        free(context);
        return calogErrOomE;
    }
    luaL_openlibs(L);
    context->L      = L;
    context->broker = broker;
    context->ctxId  = ctxId;
    *(CalogLuaT **)lua_getextraspace(L) = context;

    luaL_newmetatable(L, LUA_FUNCTION_META);
    lua_pushcfunction(L, luaFunctionCall);
    lua_setfield(L, -2, "__call");
    lua_pushcfunction(L, luaFunctionGc);
    lua_setfield(L, -2, "__gc");
    lua_pop(L, 1);

    *out = context;
    return calogOkE;
}


void calogLuaDestroy(CalogLuaT *context) {
    int32_t index;

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


static CalogLuaT *luaContextOf(lua_State *L) {
    return *(CalogLuaT **)lua_getextraspace(L);
}


int32_t calogLuaExport(CalogLuaT *context, const char *globalName, CalogFnT **out) {
    lua_State *L;
    int32_t    status;

    L = context->L;
    *out = NULL;
    lua_getglobal(L, globalName);
    if (lua_type(L, -1) != LUA_TFUNCTION) {
        lua_pop(L, 1);
        return calogErrTypeE;
    }
    status = luaExportAt(context, -1, out);
    lua_pop(L, 1);
    return status;
}


static int32_t luaExportAt(CalogLuaT *context, int idx, CalogFnT **out) {
    lua_State  *L;
    LuaExportT *export;
    int32_t     status;
    int         ref;

    L = context->L;
    *out = NULL;
    export = (LuaExportT *)malloc(sizeof(*export));
    if (export == NULL) {
        return calogErrOomE;
    }
    lua_pushvalue(L, idx);
    ref = luaL_ref(L, LUA_REGISTRYINDEX);
    export->context = context;
    export->ref     = ref;
    status = calogFnCreate(out, context->broker, luaCallableInvoke, export, luaCallableRelease, context->ctxId);
    if (status != calogOkE) {
        luaL_unref(L, LUA_REGISTRYINDEX, ref);
        free(export);
        return status;
    }
    return calogOkE;
}


int32_t calogLuaExpose(CalogLuaT *context, const char *name) {
    CalogEntryT *entry;
    BindingT     *binding;
    lua_State    *L;

    L = context->L;
    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) ? LUA_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++;

    lua_pushlightuserdata(L, binding);
    lua_pushcclosure(L, luaTrampoline, 1);
    lua_setglobal(L, name);
    return calogOkE;
}


static int luaFunctionCall(lua_State *L) {
    CalogFnT *callable;
    CalogValueT    *args;
    CalogValueT     result;
    int32_t    argCount;
    int32_t    index;
    int32_t    status;

    callable = *(CalogFnT **)lua_touserdata(L, 1);
    argCount = (int32_t)(lua_gettop(L) - 1);
    args = NULL;
    if (argCount > 0) {
        args = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT));
        if (args == NULL) {
            return luaL_error(L, "out of memory marshalling function-value args");
        }
    }
    for (index = 0; index < argCount; index++) {
        status = luaToValueDepth(L, index + 2, &args[index], 0);
        if (status != calogOkE) {
            int32_t cleanup;
            for (cleanup = 0; cleanup < index; cleanup++) {
                calogValueFree(&args[cleanup]);
            }
            free(args);
            return luaL_error(L, "failed to marshal function-value argument %d", index + 1);
        }
    }
    status = calogFnInvoke(callable, args, argCount, &result);
    for (index = 0; index < argCount; index++) {
        calogValueFree(&args[index]);
    }
    free(args);
    if (status != calogOkE) {
        size_t      len;
        const char *message;
        message = (result.type == calogStringE) ? result.as.s.bytes : "function value failed";
        len = (result.type == calogStringE) ? (size_t)result.as.s.length : strlen(message);
        lua_pushlstring(L, message, len);
        calogValueFree(&result);
        return lua_error(L);
    }
    status = luaPushValueDepth(L, &result, 0);
    calogValueFree(&result);
    if (status != calogOkE) {
        return luaL_error(L, "failed to marshal function-value result");
    }
    return 1;
}


static int luaFunctionGc(lua_State *L) {
    CalogFnT *callable;

    callable = *(CalogFnT **)lua_touserdata(L, 1);
    calogFnRelease(callable);
    return 0;
}


static int32_t luaPushValueDepth(lua_State *L, const CalogValueT *value, int32_t depth) {
    int32_t status;
    int64_t index;
    int     tbl;

    if (depth > CALOG_MAX_DEPTH) {
        return calogErrDepthE;
    }
    if (!lua_checkstack(L, LUA_MARSHAL_SLOTS)) {
        return calogErrOomE;
    }
    switch (value->type) {
        case calogNilE:
            lua_pushnil(L);
            return calogOkE;
        case calogBoolE:
            lua_pushboolean(L, value->as.b ? 1 : 0);
            return calogOkE;
        case calogIntE:
            lua_pushinteger(L, (lua_Integer)value->as.i);
            return calogOkE;
        case calogRealE:
            lua_pushnumber(L, (lua_Number)value->as.r);
            return calogOkE;
        case calogStringE:
            lua_pushlstring(L, value->as.s.bytes, (size_t)value->as.s.length);
            return calogOkE;
        case calogAggE:
            lua_createtable(L, (int)value->as.agg->arrayCount, (int)value->as.agg->pairCount);
            tbl = lua_gettop(L);
            for (index = 0; index < value->as.agg->arrayCount; index++) {
                status = luaPushValueDepth(L, &value->as.agg->array[index], depth + 1);
                if (status != calogOkE) {
                    lua_pop(L, 1);
                    return status;
                }
                lua_rawseti(L, tbl, (lua_Integer)(index + 1));
            }
            for (index = 0; index < value->as.agg->pairCount; index++) {
                status = luaPushValueDepth(L, &value->as.agg->pairs[index].key, depth + 1);
                if (status != calogOkE) {
                    lua_pop(L, 1);
                    return status;
                }
                status = luaPushValueDepth(L, &value->as.agg->pairs[index].value, depth + 1);
                if (status != calogOkE) {
                    lua_pop(L, 2);
                    return status;
                }
                lua_rawset(L, tbl);
            }
            return calogOkE;
        case calogFnE: {
            CalogFnT **slot;
            // Retain only after the __gc-bearing userdata is installed, so an
            // allocation failure (longjmp) in lua_newuserdatauv cannot strand a
            // reference that nothing will release.
            slot = (CalogFnT **)lua_newuserdatauv(L, sizeof(CalogFnT *), 0);
            *slot = value->as.fn;
            luaL_setmetatable(L, LUA_FUNCTION_META);
            calogFnRetain(value->as.fn);
            return calogOkE;
        }
    }
    return calogErrTypeE;
}


int32_t calogLuaRun(CalogLuaT *context, const char *source) {
    lua_State *L;

    L = context->L;
    if (luaL_loadstring(L, source) != LUA_OK) {
        fprintf(stderr, "lua load error: %s\n", lua_tostring(L, -1));
        lua_pop(L, 1);
        return calogErrArgE;
    }
    if (lua_pcall(L, 0, 0, 0) != LUA_OK) {
        fprintf(stderr, "lua run error: %s\n", lua_tostring(L, -1));
        lua_pop(L, 1);
        return calogErrArgE;
    }
    return calogOkE;
}


static int32_t luaToValueDepth(lua_State *L, int idx, CalogValueT *out, int32_t depth) {
    CalogLuaT *context;
    CalogAggT  *aggregate;
    CalogFnT   *callable;
    int32_t      status;
    int          absIdx;

    calogValueNil(out);
    if (depth > CALOG_MAX_DEPTH) {
        return calogErrDepthE;
    }
    absIdx = lua_absindex(L, idx);
    switch (lua_type(L, absIdx)) {
        case LUA_TNONE:
        case LUA_TNIL:
            return calogOkE;
        case LUA_TBOOLEAN:
            calogValueBool(out, lua_toboolean(L, absIdx) != 0);
            return calogOkE;
        case LUA_TNUMBER:
            if (lua_isinteger(L, absIdx)) {
                calogValueInt(out, (int64_t)lua_tointeger(L, absIdx));
            } else {
                calogValueReal(out, (double)lua_tonumber(L, absIdx));
            }
            return calogOkE;
        case LUA_TSTRING: {
            size_t      len;
            const char *bytes;
            bytes = lua_tolstring(L, absIdx, &len);
            return calogValueString(out, bytes, (int64_t)len);
        }
        case LUA_TFUNCTION:
            context = luaContextOf(L);
            status = luaExportAt(context, absIdx, &callable);
            if (status != calogOkE) {
                return status;
            }
            calogValueFn(out, callable);
            return calogOkE;
        case LUA_TTABLE: {
            lua_Integer arrayLen;
            lua_Integer i;
            status = calogAggCreate(&aggregate, calogListE);
            if (status != calogOkE) {
                return status;
            }
            if (!lua_checkstack(L, LUA_MARSHAL_SLOTS)) {
                calogAggFree(aggregate);
                return calogErrOomE;
            }
            arrayLen = (lua_Integer)lua_rawlen(L, absIdx);
            for (i = 1; i <= arrayLen; i++) {
                CalogValueT element;
                lua_rawgeti(L, absIdx, i);
                status = luaToValueDepth(L, -1, &element, depth + 1);
                lua_pop(L, 1);
                if (status != calogOkE) {
                    calogAggFree(aggregate);
                    return status;
                }
                status = calogAggPush(aggregate, &element);
                if (status != calogOkE) {
                    calogValueFree(&element);
                    calogAggFree(aggregate);
                    return status;
                }
            }
            lua_pushnil(L);
            while (lua_next(L, absIdx) != 0) {
                CalogValueT keyValue;
                CalogValueT valValue;
                if (lua_type(L, -2) == LUA_TNUMBER && lua_isinteger(L, -2)) {
                    lua_Integer k;
                    k = lua_tointeger(L, -2);
                    if (k >= 1 && k <= arrayLen) {
                        lua_pop(L, 1);
                        continue;
                    }
                }
                status = luaToValueDepth(L, -2, &keyValue, depth + 1);
                if (status != calogOkE) {
                    lua_pop(L, 2);
                    calogAggFree(aggregate);
                    return status;
                }
                status = luaToValueDepth(L, -1, &valValue, depth + 1);
                if (status != calogOkE) {
                    calogValueFree(&keyValue);
                    lua_pop(L, 2);
                    calogAggFree(aggregate);
                    return status;
                }
                status = calogAggSet(aggregate, &keyValue, &valValue);
                if (status != calogOkE) {
                    calogValueFree(&keyValue);
                    calogValueFree(&valValue);
                    lua_pop(L, 2);
                    calogAggFree(aggregate);
                    return status;
                }
                lua_pop(L, 1);
            }
            if (aggregate->pairCount == 0) {
                aggregate->kind = calogListE;
            } else if (aggregate->arrayCount == 0) {
                aggregate->kind = calogMapE;
            } else {
                aggregate->kind = calogBothE;
            }
            calogValueAgg(out, aggregate);
            return calogOkE;
        }
        case LUA_TUSERDATA: {
            CalogFnT **slot;
            // A function value previously pushed into Lua round-trips back out as
            // the same CalogFnT (retained for the new handle).
            slot = (CalogFnT **)luaL_testudata(L, absIdx, LUA_FUNCTION_META);
            if (slot == NULL) {
                return calogErrUnsupportedE;
            }
            calogFnRetain(*slot);
            calogValueFn(out, *slot);
            return calogOkE;
        }
        default:
            return calogErrUnsupportedE;
    }
}


static int luaTrampoline(lua_State *L) {
    BindingT *binding;
    CalogValueT   *args;
    CalogValueT    result;
    int32_t   argCount;
    int32_t   index;
    int32_t   status;

    binding = (BindingT *)lua_touserdata(L, lua_upvalueindex(1));
    argCount = (int32_t)lua_gettop(L);
    args = NULL;
    if (argCount > 0) {
        args = (CalogValueT *)calloc((size_t)argCount, sizeof(CalogValueT));
        if (args == NULL) {
            return luaL_error(L, "out of memory marshalling native args");
        }
    }
    for (index = 0; index < argCount; index++) {
        status = luaToValueDepth(L, index + 1, &args[index], 0);
        if (status != calogOkE) {
            int32_t cleanup;
            for (cleanup = 0; cleanup < index; cleanup++) {
                calogValueFree(&args[cleanup]);
            }
            free(args);
            return luaL_error(L, "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) {
        size_t      len;
        const char *message;
        message = (result.type == calogStringE) ? result.as.s.bytes : "native call failed";
        len = (result.type == calogStringE) ? (size_t)result.as.s.length : strlen(message);
        lua_pushlstring(L, message, len);
        calogValueFree(&result);
        return lua_error(L);
    }
    status = luaPushValueDepth(L, &result, 0);
    calogValueFree(&result);
    if (status != calogOkE) {
        return luaL_error(L, "failed to marshal native result");
    }
    return 1;
}
