#!/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":
        s = str(parameter["default"])
        return s + 'f' if '.' in s else s + '.f'
    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"])


def to_table(typedesc, variable, indent = 0):
    binding = ' ' * indent + "lua_createtable(L, 0, %i);\n" % len(typedesc["fields"])
    for field in typedesc["fields"]:
        if field["type"] == "float" or field["type"] == "uint8_t":
            binding += ' ' * indent + "lua_pushnumber(L, (double)(%s));\n" % (variable + ".%s" % field["name"])
        elif field["type"] == "bool":
            binding += ' ' * indent + "lua_pushboolean(L, (%s));\n" % (variable + ".%s" % field["name"])
        elif field["type"] in api["types"]:
            binding += to_table(api["types"][field["type"]], variable + ".%s" % field["name"], indent + 4)
        else:
            raise BaseException("Unhandled return field type '%s'" % (field["type"]))
        binding += ' ' * indent + "lua_setfield(L, -2, \"%s\");\n" % field["name"]
    return binding


def from_table(typedesc, variable, indent = 0):
    binding = ""
    for field in typedesc["fields"]:
        binding += ' ' * indent + "lua_getfield(L, -1, \"%s\");\n" % field["name"]
        if field["type"] == "float" or field["type"] == "uint8_t":
            binding += ' ' * indent + "%s = (%s)(lua_tonumber(L, -1));\n" % (variable + ".%s" % field["name"], field["type"])
        elif field["type"] == "bool":
            binding += ' ' * indent + "%s = lua_toboolean(L, -1);\n" % (variable + ".%s" % field["name"])
        elif field["type"] in api["types"]:
            binding += from_table(api["types"][field["type"]], variable + ".%s" % field["name"], indent + 4)
        else:
            raise BaseException("Unhandled return field type '%s'" % (field["type"]))
        binding += ' ' * indent + "lua_pop(L, 1);\n"
    return binding


print('#include "twn_game_api.h"\n')
print('#include "minilua.h"\n')

bindings, used_converters = [], {}
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("*") and not parameter["type"] == "char *":
                binding += "    %s %s_value;\n" % (basetype, parameter["name"])
            binding += "    %s %s;\n" % (parameter["type"] if not parameter["type"].endswith("*") else 'const ' + parameter["type"], parameter["name"])
            binding += "    lua_getfield(L, 1, \"%s\");\n" % parameter["name"]

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

            if parameter["type"] == "float":
                if "default" in parameter:
                    binding += "    int is_%s_num;\n" % parameter["name"]
                    binding += "    %s = (float)(lua_tonumberx(L, -1, &is_%s_num));\n" % (parameter["name"], parameter["name"]);
                    binding += "    if (!is_%s_num) %s = %s;\n" % (parameter["name"], parameter["name"], default(parameter))
                else:
                    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"]:
                used_converters[basetype] = api["types"][basetype]
                if parameter["type"].endswith(" *"):
                    binding += "    { %s_value = to_%s(L); %s = &%s_value; }\n" % (parameter["name"], basetype.lower(), parameter["name"], parameter["name"]);
                else:
                    binding += "    %s = to_%s(L);\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"]))
        elif procedure_desc["return"] == "char *":
            binding += "    lua_pushstring(L, %s(%s));\n" % (procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
        elif type(procedure_desc["return"]) is dict or procedure_desc["return"] in api["types"]:
            type_desc = procedure_desc["return"] if type(procedure_desc["return"]) is dict else api["types"][procedure_desc["return"]]
            binding += "    %s result = %s(%s);\n" % (type_desc["c_type"], procedure, ", ".join(param["name"] for param in procedure_desc["params"]))
            binding += to_table(type_desc, "result", 4)
        else:
            raise BaseException("Unhandled return type '%s'" % (procedure_desc["return"]))
        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"

    bindings += [binding]


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

for typename, typedesc in used_converters.items():
    if "no_convert" in typedesc and typedesc["no_convert"]:
        continue

    converter = "static %s to_%s(lua_State *L) {\n" % (typename, typename.lower())
    converter += "    %s %s;\n" % (typename, typename.lower());

    if "fields" in typedesc:
        for idx, field in enumerate(typedesc["fields"]):
            converter += "    lua_getfield(L, -%i, \"%s\");\n" % (idx + 1, 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, %i);\n" % len(typedesc["fields"]);

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


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


loader = "extern void bindgen_load_%s(lua_State *L);\n" % api["name"]
loader += "void bindgen_load_%s(lua_State *L) {\n" % api["name"]
loader += "    bindgen_init();\n"
for procedure, procedure_desc in api["procedures"].items():
    loader += "    lua_pushcfunction(L, binding_%s);\n" % procedure
    loader += "    lua_setglobal(L, \"%s\");\n" % procedure

loader += "}\n"
print(loader)


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


# exceptions for the base townengine api
# TODO: is there a way to generalize it? or rather, is there any need to do so?
if api["name"] == "twn":
    contexter = "extern void bindgen_build_context(lua_State *L);\n"
    contexter += "void bindgen_build_context(lua_State *L) {\n"
    contexter += to_table(api["types"]["Context"], "ctx", 4)
    contexter += "}\n\n"

    contexter += "extern void bindgen_upload_context(lua_State *L);\n"
    contexter += "void bindgen_upload_context(lua_State *L) {\n"
    contexter += from_table(api["types"]["Context"], "ctx", 4)
    contexter += "}"

    print(contexter)
