#!/usr/bin/python

# Copyright (C) 2009-2020, Panagiotis Christopoulos Charitos and contributors.
# All rights reserved.
# Code licensed under the BSD License.
# http://www.anki3d.org/LICENSE

import os
import optparse
import xml.etree.ElementTree as et

# Globals
identation_level = 0
out_file = None


def parse_commandline():
    """ Parse the command line arguments """

    parser = optparse.OptionParser(usage="usage: %prog [options]", description="Create LUA bindings using XML")

    parser.add_option(
        "-i", "--input", dest="inp", type="string", help="specify the XML files to parse. Seperate with :")

    (options, args) = parser.parse_args()

    if not options.inp:
        parser.error("argument is missing")

    return options.inp.split(":")


def type_sig(value):
    """ Calculate the signature of a type """
    return hash(value)


def get_base_fname(path):
    """ From path/to/a/file.ext return the "file" """
    return os.path.splitext(os.path.basename(path))[0]


def wglue(txt):
    """ Write glue code to the output """
    global out_file
    global identation_level
    out_file.write("%s%s\n" % ("\t" * identation_level, txt))


def ident(number):
    """ Increase or recrease identation for the wglue """
    global identation_level
    identation_level += number


def type_is_bool(type):
    """ Check if a type is boolean """

    return type == "Bool" or type == "bool"


def type_is_number(type):
    """ Check if a type is number """

    numbers = [
        "U8", "U16", "U32", "U64", "I8", "I16", "I32", "I64", "U", "I", "PtrSize", "F32", "F64", "int", "unsigned",
        "unsigned int", "short", "unsigned short", "uint", "float", "double"
    ]

    it_is = False
    for num in numbers:
        if num == type:
            it_is = True
            break

    return it_is


def parse_type_decl(arg_txt):
    """ Parse an arg text """

    tokens = arg_txt.split(" ")
    tokens_size = len(tokens)

    type = tokens[tokens_size - 1]
    type_len = len(type)
    is_ptr = False
    is_ref = False
    if type[type_len - 1] == "&":
        is_ref = True
    elif type[type_len - 1] == "*":
        is_ptr = True

    if is_ref or is_ptr:
        type = type[:-1]

    is_const = False
    if tokens[0] == "const":
        is_const = True

    return (type, is_ref, is_ptr, is_const)


def ret(ret_el):
    """ Push return value """

    if ret_el is None:
        wglue("return 0;")
        return

    wglue("// Push return value")

    type_txt = ret_el.text
    (type, is_ref, is_ptr, is_const) = parse_type_decl(type_txt)

    if is_ptr:
        wglue("if(ANKI_UNLIKELY(ret == nullptr))")
        wglue("{")
        ident(1)
        wglue("lua_pushstring(l, \"Glue code returned nullptr\");")
        wglue("return -1;")
        ident(-1)
        wglue("}")
        wglue("")

    if type_is_bool(type):
        wglue("lua_pushboolean(l, ret);")
    elif type_is_number(type):
        wglue("lua_pushnumber(l, lua_Number(ret));")
    elif type == "char" or type == "CString":
        wglue("lua_pushstring(l, &ret[0]);")
    elif type == "Error":
        wglue("if(ANKI_UNLIKELY(ret))")
        wglue("{")
        ident(1)
        wglue("lua_pushstring(l, \"Glue code returned an error\");")
        wglue("return -1;")
        ident(-1)
        wglue("}")
        wglue("")
        wglue("lua_pushnumber(l, lua_Number(ret));")
    else:
        if is_ptr or is_ref:
            wglue("voidp = lua_newuserdata(l, sizeof(LuaUserData));")
            wglue("ud = static_cast<LuaUserData*>(voidp);")
            wglue("luaL_setmetatable(l, \"%s\");" % type)

            wglue("extern LuaUserDataTypeInfo luaUserDataTypeInfo%s;" % type)
            if is_ptr:
                wglue("ud->initPointed(&luaUserDataTypeInfo%s, const_cast<%s*>(ret));" % (type, type))
            elif is_ref:
                wglue("ud->initPointed(&luaUserDataTypeInfo%s, const_cast<%s*>(&ret));" % (type, type))
        else:
            wglue("size = LuaUserData::computeSizeForGarbageCollected<%s>();" % type)
            wglue("voidp = lua_newuserdata(l, size);")
            wglue("luaL_setmetatable(l, \"%s\");" % type)

            wglue("ud = static_cast<LuaUserData*>(voidp);")
            wglue("extern LuaUserDataTypeInfo luaUserDataTypeInfo%s;" % type)
            wglue("ud->initGarbageCollected(&luaUserDataTypeInfo%s);" % type)

            wglue("::new(ud->getData<%s>()) %s(std::move(ret));" % (type, type))

    wglue("")
    wglue("return 1;")


def arg(arg_txt, stack_index, index):
    """ Write the pop code for a single argument """

    (type, is_ref, is_ptr, is_const) = parse_type_decl(arg_txt)

    if type_is_bool(type) or type_is_number(type):
        wglue("%s arg%d;" % (type, index))
        wglue("if(ANKI_UNLIKELY(LuaBinder::checkNumber(l, %d, arg%d)))" % (stack_index, index))
        wglue("{")
        ident(1)
        wglue("return -1;")
        ident(-1)
        wglue("}")
    elif type == "char" or type == "CString":
        wglue("const char* arg%d;" % index)
        wglue("if(ANKI_UNLIKELY(LuaBinder::checkString(l, %d, arg%d)))" % (stack_index, index))
        wglue("{")
        ident(1)
        wglue("return -1;")
        ident(-1)
        wglue("}")
    else:
        wglue("extern LuaUserDataTypeInfo luaUserDataTypeInfo%s;" % type)
        wglue("if(ANKI_UNLIKELY(LuaBinder::checkUserData(l, %d, luaUserDataTypeInfo%s, ud)))" % (stack_index, type))
        wglue("{")
        ident(1)
        wglue("return -1;")
        ident(-1)
        wglue("}")
        wglue("")

        wglue("%s* iarg%d = ud->getData<%s>();" % (type, index, type))

        if is_ptr:
            wglue("%s arg%d(iarg%d);" % (arg_txt, index, index))
        else:
            wglue("%s arg%d(*iarg%d);" % (arg_txt, index, index))


def args(args_el, stack_index):
    """ Write the pop code for argument parsing and return the arg list """

    if args_el is None:
        return ""

    wglue("// Pop arguments")
    arg_index = 0

    # Do the work
    args_str = ""
    arg_index = 0
    for arg_el in args_el.iter("arg"):
        arg(arg_el.text, stack_index, arg_index)
        args_str += "arg%d, " % arg_index
        wglue("")
        stack_index += 1
        arg_index += 1

    if len(args_str) > 0:
        args_str = args_str[:-2]

    return args_str


def count_args(args_el):
    """ Count the number of arguments """

    if args_el is None:
        return 0

    count = 0
    for arg_el in args_el.iter("arg"):
        count += 1

    return count


def check_args(args_el, bias):
    """ Check number of args. Call that first because it throws error """

    if args_el is not None:
        count = bias + count_args(args_el)
    else:
        count = bias

    wglue("if(ANKI_UNLIKELY(LuaBinder::checkArgsCount(l, %d)))" % count)
    wglue("{")
    ident(1)
    wglue("return -1;")
    ident(-1)
    wglue("}")
    wglue("")


def get_meth_alias(meth_el):
    """ Return the method alias """

    meth_name = meth_el.get("name")

    if meth_name == "operator+":
        meth_alias = "__add"
    elif meth_name == "operator-":
        meth_alias = "__sub"
    elif meth_name == "operator*":
        meth_alias = "__mul"
    elif meth_name == "operator/":
        meth_alias = "__div"
    elif meth_name == "operator==":
        meth_alias = "__eq"
    elif meth_name == "operator<":
        meth_alias = "__lt"
    elif meth_name == "operator<=":
        meth_alias = "__le"
    elif meth_name == "operator>":
        meth_alias = "__gt"
    elif meth_name == "operator>=":
        meth_alias = "__ge"
    elif meth_name == "operator=":
        meth_alias = "copy"
    else:
        meth_alias = meth_name

    meth_alias_txt = meth_el.get("alias")
    if meth_alias_txt is not None:
        meth_alias = meth_alias_txt

    return meth_alias


def write_local_vars():
    wglue("LuaUserData* ud;")
    wglue("(void)ud;")
    wglue("void* voidp;")
    wglue("(void)voidp;")
    wglue("PtrSize size;")
    wglue("(void)size;")
    wglue("")


def method(class_name, meth_el):
    """ Handle a method """

    meth_name = meth_el.get("name")
    meth_alias = get_meth_alias(meth_el)

    wglue("/// Pre-wrap method %s::%s." % (class_name, meth_name))
    wglue("static inline int pwrap%s%s(lua_State* l)" % (class_name, meth_alias))
    wglue("{")
    ident(1)
    write_local_vars()

    check_args(meth_el.find("args"), 1)

    # Get this pointer
    wglue("// Get \"this\" as \"self\"")
    wglue("if(LuaBinder::checkUserData(l, 1, luaUserDataTypeInfo%s, ud))" % class_name)
    wglue("{")
    ident(1)
    wglue("return -1;")
    ident(-1)
    wglue("}")
    wglue("")
    wglue("%s* self = ud->getData<%s>();" % (class_name, class_name))
    wglue("")

    args_str = args(meth_el.find("args"), 2)

    # Return value
    ret_txt = None
    ret_el = meth_el.find("return")
    if ret_el is not None:
        ret_txt = ret_el.text

    # Method call
    wglue("// Call the method")
    call = meth_el.find("overrideCall")
    if call is not None:
        call = call.text

    if call is not None:
        wglue("%s" % call)
    else:
        if ret_txt is None:
            wglue("self->%s(%s);" % (meth_name, args_str))
        else:
            wglue("%s ret = self->%s(%s);" % (ret_txt, meth_name, args_str))

    wglue("")
    ret(ret_el)

    ident(-1)
    wglue("}")
    wglue("")

    # Write the actual function
    wglue("/// Wrap method %s::%s." % (class_name, meth_name))
    wglue("static int wrap%s%s(lua_State* l)" % (class_name, meth_alias))
    wglue("{")
    ident(1)
    wglue("int res = pwrap%s%s(l);" % (class_name, meth_alias))
    wglue("if(res >= 0)")
    wglue("{")
    ident(1)
    wglue("return res;")
    ident(-1)
    wglue("}")
    wglue("")
    wglue("lua_error(l);")
    wglue("return 0;")
    ident(-1)
    wglue("}")
    wglue("")


def static_method(class_name, meth_el):
    """ Handle a static method """

    meth_name = meth_el.get("name")
    meth_alias = get_meth_alias(meth_el)

    wglue("/// Pre-wrap static method %s::%s." % (class_name, meth_name))
    wglue("static inline int pwrap%s%s(lua_State* l)" % (class_name, meth_alias))
    wglue("{")
    ident(1)
    write_local_vars()

    check_args(meth_el.find("args"), 0)

    # Args
    args_str = args(meth_el.find("args"), 1)

    # Return value
    ret_txt = None
    ret_el = meth_el.find("return")
    if ret_el is not None:
        ret_txt = ret_el.text

    # Method call
    wglue("// Call the method")
    if ret_txt is None:
        wglue("%s::%s(%s);" % (class_name, meth_name, args_str))
    else:
        wglue("%s ret = %s::%s(%s);" % (ret_txt, class_name, meth_name, args_str))

    wglue("")
    ret(ret_el)

    ident(-1)
    wglue("}")
    wglue("")

    # Write the actual function
    wglue("/// Wrap static method %s::%s." % (class_name, meth_name))
    wglue("static int wrap%s%s(lua_State* l)" % (class_name, meth_alias))
    wglue("{")
    ident(1)
    wglue("int res = pwrap%s%s(l);" % (class_name, meth_alias))
    wglue("if(res >= 0)")
    wglue("{")
    ident(1)
    wglue("return res;")
    ident(-1)
    wglue("}")
    wglue("")
    wglue("lua_error(l);")
    wglue("return 0;")
    ident(-1)
    wglue("}")
    wglue("")


def constructor(constr_el, class_name, constructor_idx):
    """ Handle constructor """

    wglue("/// Pre-wrap constructor for %s." % (class_name))
    wglue("static inline int pwrap%sCtor%d(lua_State* l)" % (class_name, constructor_idx))
    wglue("{")
    ident(1)
    write_local_vars()

    check_args(constr_el.find("args"), 0)

    # Args
    args_str = args(constr_el.find("args"), 1)

    # Create new userdata
    wglue("// Create user data")

    wglue("size = LuaUserData::computeSizeForGarbageCollected<%s>();" % class_name)
    wglue("voidp = lua_newuserdata(l, size);")
    wglue("luaL_setmetatable(l, luaUserDataTypeInfo%s.m_typeName);" % class_name)
    wglue("ud = static_cast<LuaUserData*>(voidp);")
    wglue("extern LuaUserDataTypeInfo luaUserDataTypeInfo%s;" % class_name)
    wglue("ud->initGarbageCollected(&luaUserDataTypeInfo%s);" % class_name)
    wglue("::new(ud->getData<%s>()) %s(%s);" % (class_name, class_name, args_str))
    wglue("")

    wglue("return 1;")

    ident(-1)
    wglue("}")
    wglue("")


def constructors(constructors_el, class_name):
    """ Wrap all constructors """

    idx = 0
    func_names_and_arg_counts = []

    # Create the pre-wrap C functions
    for constructor_el in constructors_el.iter("constructor"):
        arg_count = count_args(constructor_el.find("args"))

        # Iterate all arg counts and make sure there are no duplicates
        for i in range(idx):
            if func_names_and_arg_counts[i][1] == arg_count:
                raise Exception("Every constructor overload should have a unique arg count. class: %s" % class_name)

        constructor(constructor_el, class_name, idx)
        func_names_and_arg_counts.append(["pwrap%sCtor%d" % (class_name, idx), arg_count])
        idx += 1

    if idx == 0:
        raise Exception("Found no <constructor>")

    # Create the landing function
    wglue("/// Wrap constructors for %s." % class_name)
    wglue("static int wrap%sCtor(lua_State* l)" % class_name)
    wglue("{")
    ident(1)
    if idx == 1:
        wglue("int res = pwrap%sCtor0(l);" % class_name)
    else:
        wglue("// Chose the right overload")
        wglue("const int argCount = lua_gettop(l);")
        wglue("int res = 0;")
        wglue("switch(argCount)")
        wglue("{")
        for name_and_arg_count in func_names_and_arg_counts:
            func_name = name_and_arg_count[0]
            arg_count = name_and_arg_count[1]
            wglue("case %d:" % arg_count)
            wglue("res = %s(l); break;" % func_name)

        wglue("default:")
        wglue("lua_pushfstring(l, \"Wrong overloaded new. Wrong number of arguments: %d\", argCount);")
        wglue("res = -1;")
        wglue("}")
    wglue("")

    wglue("if(res >= 0)")
    wglue("{")
    ident(1)
    wglue("return res;")
    ident(-1)
    wglue("}")
    wglue("")
    wglue("lua_error(l);")
    wglue("return 0;")
    ident(-1)
    wglue("}")
    wglue("")


def destructor(class_name):
    """ Create a destructor """

    wglue("/// Wrap destructor for %s." % (class_name))
    wglue("static int wrap%sDtor(lua_State* l)" % class_name)
    wglue("{")
    ident(1)
    write_local_vars()

    check_args(None, 1)

    wglue("if(ANKI_UNLIKELY(LuaBinder::checkUserData(l, 1, luaUserDataTypeInfo%s, ud)))" % class_name)
    wglue("{")
    ident(1)
    wglue("return -1;")
    ident(-1)
    wglue("}")
    wglue("")

    wglue("if(ud->isGarbageCollected())")
    wglue("{")
    ident(1)
    wglue("%s* inst = ud->getData<%s>();" % (class_name, class_name))
    wglue("inst->~%s();" % class_name)
    ident(-1)
    wglue("}")
    wglue("")

    wglue("return 0;")

    ident(-1)
    wglue("}")
    wglue("")


def class_(class_el):
    """ Create a class """

    class_name = class_el.get("name")

    # Write serializer
    serialize = class_el.get("serialize") is not None and class_el.get("serialize") == "true"
    if serialize:
        # Serialize
        serialize_cb_name = "serialize%s" % class_name
        wglue("/// Serialize %s" % class_name)
        wglue("static void %s(LuaUserData& self, void* data, PtrSize& size)" % serialize_cb_name)
        wglue("{")
        ident(1)
        wglue("%s* obj = self.getData<%s>();" % (class_name, class_name))
        wglue("obj->serialize(data, size);")
        ident(-1)
        wglue("}")
        wglue("")

        # Deserialize
        deserialize_cb_name = "deserialize%s" % class_name
        wglue("/// De-serialize %s" % class_name)
        wglue("static void %s(const void* data, LuaUserData& self)" % deserialize_cb_name)
        wglue("{")
        ident(1)
        wglue("ANKI_ASSERT(data);")
        wglue("%s* obj = self.getData<%s>();" % (class_name, class_name))
        wglue("::new(obj) %s();" % class_name)
        wglue("obj->deserialize(data);")
        ident(-1)
        wglue("}")
        wglue("")
    else:
        serialize_cb_name = "nullptr"
        deserialize_cb_name = "nullptr"

    # Write the type info
    wglue("LuaUserDataTypeInfo luaUserDataTypeInfo%s = {" % class_name)
    ident(1)
    wglue("%d, \"%s\", LuaUserData::computeSizeForGarbageCollected<%s>(), %s, %s" %
          (type_sig(class_name), class_name, class_name, serialize_cb_name, deserialize_cb_name))
    ident(-1)
    wglue("};")
    wglue("")

    # Specialize the getDataTypeInfoFor
    wglue("template<>")
    wglue("const LuaUserDataTypeInfo& LuaUserData::getDataTypeInfoFor<%s>()" % class_name)
    wglue("{")
    ident(1)
    wglue("return luaUserDataTypeInfo%s;" % class_name)
    ident(-1)
    wglue("}")
    wglue("")

    # Constructor declarations
    has_constructor = False
    constructors_el = class_el.find("constructors")
    if constructors_el is not None:
        has_constructor = True
        constructors(constructors_el, class_name)

    # Destructor declarations
    if has_constructor:
        destructor(class_name)

    # Methods LUA C functions declarations
    meth_names_aliases = []
    meths_el = class_el.find("methods")
    if meths_el is not None:
        for meth_el in meths_el.iter("method"):
            is_static = meth_el.get("static")
            is_static = is_static is not None and is_static == "1"

            if is_static:
                static_method(class_name, meth_el)
            else:
                method(class_name, meth_el)

            meth_name = meth_el.get("name")
            meth_alias = get_meth_alias(meth_el)
            meth_names_aliases.append([meth_name, meth_alias, is_static])

    # Start class declaration
    wglue("/// Wrap class %s." % class_name)
    wglue("static inline void wrap%s(lua_State* l)" % class_name)
    wglue("{")
    ident(1)
    wglue("LuaBinder::createClass(l, &luaUserDataTypeInfo%s);" % class_name)

    # Register constructor
    if has_constructor:
        wglue("LuaBinder::pushLuaCFuncStaticMethod(l, luaUserDataTypeInfo%s.m_typeName, \"new\", wrap%sCtor);" %
              (class_name, class_name))

    # Register destructor
    if has_constructor:
        wglue("LuaBinder::pushLuaCFuncMethod(l, \"__gc\", wrap%sDtor);" % class_name)

    # Register methods
    if len(meth_names_aliases) > 0:
        for meth_name_alias in meth_names_aliases:
            meth_alias = meth_name_alias[1]
            is_static = meth_name_alias[2]
            if is_static:
                wglue("LuaBinder::pushLuaCFuncStaticMethod(l, luaUserDataTypeInfo%s.m_typeName, \"%s\", wrap%s%s);" %
                      (class_name, meth_alias, class_name, meth_alias))
            else:
                wglue("LuaBinder::pushLuaCFuncMethod(l, \"%s\", wrap%s%s);" % (meth_alias, class_name, meth_alias))

    wglue("lua_settop(l, 0);")

    ident(-1)
    wglue("}")
    wglue("")


def function(func_el):
    """ Handle a plain function """

    func_name = func_el.get("name")
    func_alias = get_meth_alias(func_el)

    wglue("/// Pre-wrap function %s." % func_name)
    wglue("static inline int pwrap%s(lua_State* l)" % func_alias)
    wglue("{")
    ident(1)
    write_local_vars()

    check_args(func_el.find("args"), 0)

    # Args
    args_str = args(func_el.find("args"), 1)

    # Return value
    ret_txt = None
    ret_el = func_el.find("return")
    if ret_el is not None:
        ret_txt = ret_el.text

    # Call
    wglue("// Call the function")
    call = func_el.find("overrideCall")
    if call is not None:
        call = call.text

    if call is not None:
        wglue("%s" % call)
    else:
        if ret_txt is None:
            wglue("%s(%s);" % (func_name, args_str))
        else:
            wglue("%s ret = %s(%s);" % (ret_txt, func_name, args_str))

    wglue("")
    ret(ret_el)

    ident(-1)
    wglue("}")
    wglue("")

    # Write the actual function
    wglue("/// Wrap function %s." % func_name)
    wglue("static int wrap%s(lua_State* l)" % func_alias)
    wglue("{")
    ident(1)
    wglue("int res = pwrap%s(l);" % func_alias)
    wglue("if(res >= 0)")
    wglue("{")
    ident(1)
    wglue("return res;")
    ident(-1)
    wglue("}")
    wglue("")
    wglue("lua_error(l);")
    wglue("return 0;")
    ident(-1)
    wglue("}")
    wglue("")


def main():
    """ Main function """

    global out_file
    filenames = parse_commandline()

    for filename in filenames:
        out_filename = get_base_fname(filename) + ".cpp"
        out_file = open(out_filename, "w")

        tree = et.parse(filename)
        root = tree.getroot()

        # Head
        head = root.find("head")
        if head is not None:
            wglue("%s" % head.text)
            wglue("")

        # Classes
        class_names = []
        for cls in root.iter("classes"):
            for cl in cls.iter("class"):
                class_(cl)
                class_names.append(cl.get("name"))

        # Functions
        func_names = []
        for fs in root.iter("functions"):
            for f in fs.iter("function"):
                function(f)
                func_names.append(f.get("name"))

        # Wrap function
        wglue("/// Wrap the module.")
        wglue("void wrapModule%s(lua_State* l)" % get_base_fname(filename))
        wglue("{")
        ident(1)
        for class_name in class_names:
            wglue("wrap%s(l);" % class_name)
        for func_name in func_names:
            wglue("LuaBinder::pushLuaCFunc(l, \"%s\", wrap%s);" % (func_name, func_name))
        ident(-1)
        wglue("}")
        wglue("")

        # Tail
        tail = root.find("tail")
        if tail is not None:
            wglue("%s" % tail.text)
            wglue("")

        out_file.close()


if __name__ == "__main__":
    main()
