#!/bin/env python3

import sys, json

with open(sys.argv[1], 'r') if sys.argv[1] != "-" else sys.stdin as f:
    api_source = f.read()

api = json.loads(api_source)


def default(parameter):
    basetype = parameter["type"].rsplit(' *', 1)[0]
    if parameter["type"] == "float":
        return parameter["default"]
    elif parameter["type"] == "bool":
        return "true" if parameter["default"] else "false"
    elif parameter["type"] == "char *":
        if parameter["default"] == {}:
            return "NULL"
        else: return '"' + parameter["default"] + '"'
    elif basetype in api["types"]:
        if parameter["type"].endswith(" *"):
            if parameter["default"] == {}:
                return "NULL"
            else:
                return "&(%s){\n%s\n    }" % \
                    (parameter["type"], ", \n".join("            .%s = %s" % (n, v) for n, v in parameter["default"].items()))
        else:
            return "(%s){\n%s\n    }" % \
                (parameter["type"], ", \n".join("            .%s = %s" % (n, v) for n, v in parameter["default"].items()))
    raise BaseException("Unhandled default value of type '%s'" % parameter["type"])


print('#include "twn_game_api.h"\n')
print('#define STB_DS_IMPLEMENTATION') # TODO: reuse implementation from the engine
print('#include <stb_ds.h>')
print('#include <lua.h>')
print('#include <lualib.h>')
print('#include <lauxlib.h>\n')

storages, converters, initializers, deinitializers = [], [], [], []

for typename, typedesc in api["types"].items():
    converter = "static %s to_%s(lua_State *L, int idx) {\n" % (typename, typename.lower())
    converter += "    %s %s;\n" % (typename, typename.lower());

    if "fields" in typedesc:
        for field in typedesc["fields"]:
            converter += "    lua_getfield(L, idx, \"%s\");\n" % (field["name"]);
            if field["type"] == "float":
                converter += "    %s.%s = (float)lua_tonumber(L, -1);\n" % (typename.lower(), field["name"]);
            elif field["type"] == "uint8_t":
                converter += "    %s.%s = (uint8_t)lua_tointeger(L, -1);\n" % (typename.lower(), field["name"]);
            else:
                raise BaseException("Unhandled converter field type '%s'" % (field["type"]))
            converter += "    lua_pop(L, 1);\n";

    # TODO: wild idea: use compile time built hash table
    elif "enums" in typedesc:
        storages += ["struct %sHashItem { char *key; %s value; };\nstatic struct %sHashItem *%s_map = NULL;\n" % (typename, typename, typename, typename.lower())]

        # TODO: use arena
        for enum in typedesc["enums"]:
            initializer = "    shput(%s_map, \"%s\", %s);" % (typename.lower(), enum, typedesc["enums"][enum])
            initializers += [initializer]

        deinitializers += ["    shfree(%s_map);" % typename.lower()]

        converter += "    char *value = lua_tostring(L, -1);\n";
        converter += "        %s = shget(%s_map, value);\n" % (typename.lower(), typename.lower())
        converter += "    lua_pop(L, 1);\n";

    converter += "    return %s;\n}\n" % (typename.lower())
    converters += [converter]

print('\n'.join(storages))
print("void bindgen_init(void) {\n" + '\n'.join(initializers) + "\n}\n")
print('\n'.join(converters))

bindings = []
for procedure, procedure_desc in api["procedures"].items():
    binding = "static int binding_%s(lua_State *L) {\n" % procedure
    binding += "    luaL_checktype(L, 1, LUA_TTABLE);\n"

    if "params" in procedure_desc:
        for parameter in procedure_desc["params"]:
            basetype = parameter["type"].rsplit(' *', 1)[0]

            if parameter["type"].endswith("*"):
                binding += "    %s %s_value;\n" % (basetype, parameter["name"])
            binding += "    %s %s;\n" % (parameter["type"], parameter["name"])
            binding += "    lua_getfield(L, 1, \"%s\");\n" % parameter["name"]

            if "default" in parameter:
                binding += "    if (lua_isnoneornil(L, -1))\n"
                binding += "        %s = %s;\n" % (parameter["name"], default(parameter))
                binding += "    else\n    "

            if parameter["type"] == "float":
                binding += "    %s = (float)lua_tonumber(L, -1);\n" % (parameter["name"]);
            elif parameter["type"] == "bool":
                binding += "    %s = lua_toboolean(L, -1);\n" % (parameter["name"]);
            elif parameter["type"] == "char *":
                binding += "    %s = lua_tostring(L, -1);\n" % (parameter["name"]);
            elif basetype in api["types"]:
                if parameter["type"].endswith(" *"):
                    binding += "    { %s_value = to_%s(L, -1); %s = &%s_value; }\n" % (parameter["name"], basetype.lower(), parameter["name"], parameter["name"]);
                else:
                    binding += "    %s = to_%s(L, -1);\n" % (parameter["name"], basetype.lower());
            else:
                raise BaseException("Unhandled parameter type '%s'" % (parameter["type"]))

    if "return" in procedure_desc:
        if procedure_desc["return"] == "bool":
            binding += "    lua_pushboolean(L, (int)%s(%s));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
        elif procedure_desc["return"] == "float":
            binding += "    lua_pushnumber(L, (double)%s(%s));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
        else:
            binding += "    lua_warning(L, \"Unhandled return type!\", 0);\n"
        binding += "    return 1;\n}\n"
    else:
        binding += "    %s(%s);\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
        binding += "    return 0;\n}\n"

    print(binding)


loader = "void bindgen_load_%s(lua_State *L) {\n" % api["name"]
modules = set(api["procedures"][procedure]["module"] for procedure in api["procedures"])
for module in modules:
    loader += "    bindgen_init();\n"
    loader += "    lua_newtable(L);\n"
    for procedure, procedure_desc in api["procedures"].items():
        if procedure_desc["module"] == module:
            loader += "    lua_pushstring(L, \"%s\");\n" % procedure_desc["symbol"]
            loader += "    lua_pushcfunction(L, binding_%s);\n" % procedure
            loader += "    lua_settable(L, -3);\n"
    loader += "    lua_setglobal(L, \"%s\");\n" % module

loader += "}"
print(loader)


unloader = "void bindgen_unload_%s(lua_State *L) {\n" % api["name"]
unloader += '\n'.join(deinitializers)
unloader += "}"
print(unloader)
