// scripting integration using lua

const std = @import("std");
pub const lua = @import("lua");
const core = @import("core.zig");
const startup_script = @embedFile("lua/startup.lua");
const core_script = @embedFile("lua/core.lua");
const ecs = @import("ecs.zig");
const ComponentRef = @import("script/ComponentRef.zig");
const ComponentRegistration = @import("script/ComponentRegistration.zig");

pub const script_bindings = @import("script_bindings.zig");

const c = lua.c;

var gLuaState: lua.LuaState = undefined;
var gLuaAllocator: std.mem.Allocator = undefined;

const luaRegLibs: []const c.luaL_Reg = &.{
    .{ .name = "print", .func = printWrapper },
    .{ .name = null, .func = null },
};

// binds the default print() function in lua to print to the console
fn printWrapper(l: ?*lua.c.lua_State) callconv(.C) i32 {
    const state: lua.LuaState = .{ .l = l };
    const argc = state.getTop();

    if (core.getLogger() == null) {
        core.printRaw("> ", .{});
    }

    if (argc >= 1) {
        core.printRaw("[SCRIPT   ]: ", .{});
    }

    var i: i32 = 1;
    while (i <= argc) : (i += 1) {
        if (state.isString(i)) {
            if (i > 1) {
                core.printRaw(" ", .{});
            }
            core.printRaw("{s}", .{state.toString(i)});
        } else if (state.isUserdata(i)) {
            const s = state.toStringL(i);
            state.pop(1);
            core.printRaw("{s}", .{s});
        }
    }

    core.printRaw("\n", .{});
    state.pop(argc);

    return 0;
}

// initialization of the lua scripting interface.
// this interface is only threadsafe to operate on from the main systems thread (at this time).
pub fn start_lua(allocator: std.mem.Allocator) !void {
    gLuaAllocator = allocator;
    gLuaState = try lua.LuaState.init(.{});
    try lua.pod.setupFormatBuffer(allocator);

    // overload and hook into global functions
    _ = gLuaState.getGlobal("_G");
    c.luaL_setfuncs(gLuaState.l, luaRegLibs.ptr, 0);
    gLuaState.pop(1);

    try lua.pod.registerPodType(&gLuaState, ecs.Entity);
    try lua.pod.registerPodType(&gLuaState, ComponentRegistration);

    try script_bindings.registerTypes();

    try gLuaState.loadString(startup_script);
    try gLuaState.pcall();

    try gLuaState.loadString(core_script);
    try gLuaState.pcall();
}

pub fn createLuaComponentDefinitions(globalName: []const u8, container: ecs.EcsContainerRef, luaNew: anytype) !void {
    const reg = try gLuaState.newZigUserdata(ComponentRegistration);
    reg.ref = container;
    reg.name = globalName;
    reg.luaNew = luaNew;
    try gLuaState.setGlobal(globalName);
}

pub fn registerComponent(comptime Component: type, container: ecs.EcsContainerRef) !void {
    const ReferenceType = ComponentRef.ComponentReferenceType(Component);
    try createLuaComponentDefinitions(@ptrCast(Component.ComponentName), container, ReferenceType.luaNew);
    try ReferenceType.registerType(getState());
}

pub fn shutdown_lua() void {
    lua.pod.shutdownFormatBuffer();
    gLuaState.deinit();
}

pub fn getState() *lua.LuaState {
    return &gLuaState;
}

fn findScriptBasepath(s: []const u8) []const u8 {
    var i: usize = s.len - 1;

    while (i > 0) : (i -= 1) {
        if (s[i] == '\\' or s[i] == '/') {
            break;
        }
    }

    if (s[i] == '\\' or s[i] == '/') {
        return s[i + 1 ..];
    }

    return s;
}

pub fn loadTypes(scriptPath: []const u8) !void {
    // 1. scan filesystem for all files under the script path
    var fileList = try core.fs().listAllSubpaths(gLuaAllocator, scriptPath);
    defer fileList.deinit();
    for (fileList.data.items, 0..) |f, i| {
        // 2. enforce naming scheme for each script.
        const basePath = findScriptBasepath(f);

        // core.engine_log("checking script file: {s} path:{s} base {s}", .{ f, fileList.sources.items[i], basePath });
        // 3. for each one, load the script and assign them based on name.
        if (std.ascii.isUpper(basePath[0])) {
            core.engine_log("loading script file: {s} path:{s}", .{ f, fileList.sources.items[i] });
            const scriptFile = try core.fs().loadFile(f);
            defer core.fs().unmap(scriptFile);
            try gLuaState.loadString(scriptFile.bytes);
            try gLuaState.pcall();
        }
    }
}

pub fn runScriptFile(scriptPath: []const u8) !void {
    const scriptFile = try core.fs().loadFile(scriptPath);
    defer core.fs().unmap(scriptFile);
    try gLuaState.loadString(scriptFile.bytes);
    try gLuaState.pcall();
}
