import os
import sys
import argparse

from pygccxml import parser, declarations

class wType:
    def __init__(self, c_str):
        self.c_str = c_str
        self.wrapped_types = ["bint", "double", "int", "unsigned int",
                            "Coord", "IntCoord", "Dim2", "size_t"]

    def get_C(self):
        """returns plain c string"""
        return self.c_str

    def get_Cython_type(self):
        """Returns corresponding Cython type string"""
        typestr= self.get_C()
        typestr= typestr.replace(" const ", " ")
        if typestr.find("::Geom::")==0:
            typestr = typestr[len("::Geom::"):]
        if typestr == "bool":
            typestr = "bint"
        return typestr
        
    def get_Python_function_argument(self):
        """Returns argument type to Python function call"""

        typestr = self.get_Cython_type()
        if typestr[-1]=="&":
            typestr = typestr[:-2]
        if typestr in self.wrapped_types:
            return typestr+" "
        return "cy_"+typestr+" "
    def get_Python_return(self):
        """ Returns a string used to wrap a function returning this type
        returns a string to be called with format( 'function call to c function ')
        """
        typestr = self.get_Cython_type()
        
        if typestr[-1] == "&":
            typestr = typestr[:-2]
        if typestr in self.wrapped_types:
            return "return {}"
        if typestr == "void":
            return "{}"
        if typestr[-1] == "*":
            typestr = typestr[:-2]+"_p"
        return "return wrap_"+typestr+"({})"
        
    def get_wrap_method(self, delim):
        """Returns wrap method in a form of a list of lines"""
        ret = []
        typestr = self.get_Cython_type()
        
        if typestr[-1] == "&":
            typestr = typestr[:-2]
        if typestr in self.wrapped_types:
            return []
        if typestr == "void":
            return []
        if typestr[-1] == "*":
            typestr = typestr[:-2]+"_p"
        ret.append("cdef cy_{} wrap_{}({} p):".format(typestr, typestr, typestr))
        ret.append(delim+"cdef {} * retp = new {}()".format(typestr, typestr))
        ret.append(delim+"retp[0] = p")
        ret.append(delim+"cdef cy_{} r = cy_{}.__new__(cy_{})".format(typestr, typestr, typestr))
        ret.append(delim+"r.thisptr = retp")
        ret.append(delim+"return r")
        return ret
        
    def get_Python_pass_argument(self):
        """ Returns argument to be passed to C function, to be called with .format(arg_name)"""
        typestr = self.get_Cython_type()
        if typestr[-1]=="&":
            typestr = typestr[:-2]
        if typestr in self.wrapped_types:
            return "{}"
        if typestr[-1] == "*":
            return "{}.thisptr"
        return "deref( {}.thisptr )"

class CythonWrapper:
    def __init__(self, gn):
        self.global_namespace = gn
        self.delim = '    '
    
    def wrap_constructor(self, constructor, types_dict):
        pxd_lines = []
        pyx_lines = []
        
        pxd_arguments = []
        for argument_type in constructor.argument_types:
            pxd_arguments.append( types_dict[argument_type.decl_string].get_Cython_type() )
        pxd_argument_str = "({})".format(", ".join(pxd_arguments))
        
        pxd_function_str = constructor.name+pxd_argument_str
        pxd_lines.append(self.delim*2+pxd_function_str)
        
        #python function
        arguments_python = ["self"]
        for argument in constructor.arguments:
            arguments_python.append(types_dict[argument.type.decl_string].get_Python_function_argument()+argument.name)
        declaration = "def {}".format("__init__")+"({}):".format(", ".join(arguments_python))
        
        arguments_cython = []
        for argument in constructor.arguments:
            arguments_cython.append(types_dict[argument.type.decl_string].get_Python_pass_argument().format(argument.name))
        function_call = "new "+constructor.name+"({})".format(", ".join(arguments_cython))
        
        return_statement = "self.thisptr = {}".format(function_call)
        pyx_lines.append(self.delim+declaration)
        pyx_lines.append(self.delim*2 + return_statement)
        
        return (pxd_lines, pyx_lines)

    def wrap_operator(self, operator, types_dict):
        pxd_lines = []
        pyx_lines = []

        pxd_arguments = []
        for argument_type in operator.argument_types:
            pxd_arguments.append( types_dict[argument_type.decl_string].get_Cython_type() )
        pxd_argument_str = "({})".format(", ".join(pxd_arguments))
        boost_wrapped = ["operator{}=".format(i) for i in ["+", "-", "*", "/", "|", "&"]]
        pxd_function_str = types_dict[operator.return_type.decl_string].get_Cython_type()+\
                            " "+operator.name+pxd_argument_str
        
        if not (operator.name in boost_wrapped):
            pxd_lines.append(self.delim*2+pxd_function_str)

        else:
            pxd_lines.append(self.delim*2+"#"+pxd_function_str)

            pxd_function_str = types_dict[operator.return_type.decl_string].get_Cython_type()+\
            " "+operator.name[:-1]+pxd_argument_str
            pxd_lines.append(self.delim*2+pxd_function_str)


        special_methods = {
            "+" : "__add__",
            "-" : "__sub__",
            "*" : "__mul__",
            "/" : "__div__",
            "|" : "__or__",
            "&" : "__and__",
            "[]": "__getitem__",
            "()": "__call__",
            "==": "__richcmp__",
            "!=": "__richcmp__"
        }
        
        if operator.name in boost_wrapped:
            operation = operator.name[-2]
        else:
            operation = operator.name[ len("operator"): ]
            
        if operation in special_methods:
            python_name = special_methods[operation]
        else:
            python_name = operator.name
        
        arguments_python = ["self"]
        for argument in operator.arguments:
            arguments_python.append(types_dict[argument.type.decl_string].get_Python_function_argument()+argument.name)
        declaration = "def {}".format(python_name)+"({}):".format(", ".join(arguments_python))
        
        arguments_cython = []
        for argument in operator.arguments:
            arguments_cython.append(types_dict[argument.type.decl_string].get_Python_pass_argument().format(argument.name))
        function_call = "deref( self.thisptr ) "+operation+" {}".format(", ".join(arguments_cython))
        
        return_statement = types_dict[operator.return_type.decl_string].get_Python_return().format(function_call)
        
        if (operator.arguments == []) and (operation == '-'):
            declaration = "def __neg__(self):"
        pyx_lines.append(self.delim+declaration)
        pyx_lines.append(self.delim*2 + return_statement)

        return (pxd_lines, pyx_lines)

    def wrap_method(self, method, types_dict):
        pxd_lines = []
        pyx_lines = []
        
        pxd_arguments = []
        for argument_type in method.argument_types:
            pxd_arguments.append( types_dict[argument_type.decl_string].get_Cython_type() )
        pxd_argument_str = "({})".format(", ".join(pxd_arguments))
        
        
        pxd_function_str = types_dict[method.return_type.decl_string].get_Cython_type()+\
                            " "+method.name+pxd_argument_str
        pxd_lines.append(self.delim*2+pxd_function_str)
        
        #python function
        arguments_python = ["self"]
        for argument in method.arguments:
            arguments_python.append(types_dict[argument.type.decl_string].get_Python_function_argument()+argument.name)
        declaration = "def {}".format(method.name)+"({}):".format(", ".join(arguments_python))
        
        arguments_cython = []   
        for argument in method.arguments:
            arguments_cython.append(types_dict[argument.type.decl_string].get_Python_pass_argument().format(argument.name))
        function_call = "self.thisptr."+method.name+"({})".format(", ".join(arguments_cython))
        
        return_statement = types_dict[method.return_type.decl_string].get_Python_return().format(function_call)
        pyx_lines.append(self.delim+declaration)
        pyx_lines.append(self.delim*2 + return_statement)
        
        return (pxd_lines, pyx_lines)
    
    def wrap_free_function(self, function, types_dict):
        pxd_lines = []
        pyx_lines = []
        decl_lines = []
                                
        pxd_arguments = []
        for argument_type in function.argument_types:
            pxd_arguments.append( types_dict[argument_type.decl_string].get_Cython_type() )
        pxd_argument_str = "({})".format(", ".join(pxd_arguments))
        
        
        pxd_function_str = types_dict[function.return_type.decl_string].get_Cython_type()+\
                            " "+function.name+pxd_argument_str
        pxd_lines.append(self.delim+pxd_function_str)
        
        #python function
        arguments_python = []
        for argument in function.arguments:
            arguments_python.append(types_dict[argument.type.decl_string].get_Python_function_argument()+argument.name)
        declaration = "def cy_{}".format(function.name)+"({}):".format(", ".join(arguments_python))
        
        decl_lines.append("from _cy_??? import cy_{0} as {0}".format(function.name))
        
        arguments_cython = []   
        for argument in function.arguments:

            arguments_cython.append(types_dict[argument.type.decl_string].get_Python_pass_argument().format(argument.name))
        function_call = function.name+"({})".format(", ".join(arguments_cython))
        
        return_statement = types_dict[function.return_type.decl_string].get_Python_return().format(function_call)
        pyx_lines.append(declaration)
        pyx_lines.append(self.delim + return_statement)
        
        return (pxd_lines, pyx_lines, decl_lines)

    
    def wrap_class(self, class_name, class_file):
        pxd_lines = []
        pyx_lines = []
        decl_lines = []
        other_lines = []

        pxd_lines.append("cdef extern from \"2geom/{}\" namespace \"Geom\":".format(class_file))
        pxd_lines.append(self.delim+"cdef cppclass {}:".format(class_name))
        pyx_lines.append("cdef class cy_{}:".format(class_name))
        pyx_lines.append(self.delim+"cdef {}* thisptr".format(class_name))
        #get_types
        types_dict = self.collect_types(class_name)
        #parse
        c_class = self.global_namespace.namespace(name = "Geom").class_(name=class_name)

        for member in c_class.get_members():
            if isinstance(member, declarations.calldef.constructor_t):
                pxd, pyx = self.wrap_constructor(member, types_dict)
                pxd_lines += pxd
                pyx_lines += pyx
            elif isinstance(member, declarations.calldef.member_operator_t):
                pxd, pyx = self.wrap_operator(member, types_dict)
                pxd_lines += pxd
                pyx_lines += pyx
            elif isinstance(member, declarations.calldef.member_function_t):
                pxd, pyx = self.wrap_method(member, types_dict)
                pxd_lines += pxd
                pyx_lines += pyx
                
        pyx_lines.append("#free functions:")
        for ff in self.global_namespace.namespace(name = "Geom").free_funs():
            if os.path.basename(ff.location.file_name) == class_file:
                pxd, pyx, decl = self.wrap_free_function(ff, types_dict)
                pxd_lines += map(lambda x: x[4:], pxd)
                pyx_lines += pyx
                decl_lines += decl
                
        for ff in self.global_namespace.namespace(name = "Geom").free_operators():
            if os.path.basename(ff.location.file_name) == class_file:
                pxd, pyx = self.wrap_operator(ff, types_dict)
                pxd_lines += map(lambda x: x[4:], pxd)
                pyx_lines += pyx
   
        pyx_lines.append("")
        for ctype in types_dict:
            wtype = types_dict[ctype]
            for line in wtype.get_wrap_method(self.delim):
                pyx_lines.append(line)
        print "------"
        for i in pxd_lines:
            print i
        print "------"
        for i in pyx_lines:
            print i
        print "------"
        for i in decl_lines:
            print i
            
           
    def collect_types(self, class_name):
        types_set = set()
        c_class = self.global_namespace.namespace(name = "Geom").class_(name=class_name)
        for member in c_class.get_members():
            if any([isinstance(member, declarations.calldef.constructor_t),
                    isinstance(member, declarations.calldef.member_operator_t),
                    isinstance(member, declarations.calldef.member_function_t)]):
                for a_type in member.argument_types:
                    types_set.add(a_type.decl_string) 
                if isinstance(member, declarations.calldef.member_operator_t) or isinstance(member, declarations.calldef.member_function_t):
                    types_set.add(member.return_type.decl_string)
        for ff in list(self.global_namespace.namespace(name = "Geom").free_funs()) + list(self.global_namespace.namespace(name = "Geom").free_operators()):
            for a_type in ff.argument_types:
                types_set.add(a_type.decl_string)
                types_set.add(ff.return_type.decl_string)
        types_dict = dict()
        
        for f_type in types_set:

            types_dict[f_type] = wType(f_type)
        return types_dict

def main():

    #set up argument parser
    cmd_parser = argparse.ArgumentParser()
    cmd_parser.add_argument('--lib2geom_dir', action = 'store')
    cmd_parser.add_argument('--file_name', action = 'store')
    cmd_parser.add_argument('--class_name', action = 'store')
    cmd_args = cmd_parser.parse_args()
    
    #set up pygccxml
    includes = [cmd_args.lib2geom_dir, 
                os.path.join(cmd_args.lib2geom_dir, 'src'), 
                "/usr/include/boost"]

    config = parser.config_t(compiler='gcc', 
                            include_paths=includes, 
                            cflags="")
    
    file_to_parse = [os.path.join(cmd_args.lib2geom_dir, "src", "2geom", cmd_args.file_name)] 
    
    decls = parser.parse( file_to_parse, config )
    global_namespace = declarations.get_global_namespace( decls )
    wrapper = CythonWrapper(global_namespace)
    wrapper.wrap_class(cmd_args.class_name, cmd_args.file_name)

main()

#run with
#python2 wrapper.py --lib2geom_dir ../../.. --file_name int-point.h --class_name IntPoint
#from cython-bindings directory
