# -*- coding: utf-8 -*-
"""
CLibrary.py -  Provides CLibrary class
Copyright 2010  Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.

Proxy to both CHeader and ctypes, allowing automatic type conversion and
function calling based on C header definitions.
"""


from ctypes import *
import sys


class CLibrary:
    """The CLibrary class is intended to automate much of the work in using ctypes by integrating
    header file definitions from CParser. Ths class serves as a proxy to a ctypes, adding
    a few features:
      - allows easy access to values defined via CParser
      - automatic type conversions for function calls using CParser function signatures
      - creates ctype classes based on type definitions from CParser
      
    Initialize using a ctypes shared object and a CParser:
       headers = CParser.winDefs()
       lib = CLibrary(windll.User32, headers)
      
    There are 3 ways to access library elements:
        lib(type, name) - type can be one of 'values', 'functions', 'types', 'structs', 'unions', or 'enums'.
                          Returns an object matching name. For values, the value from the headers is
                          returned. For functions, a callable object is returned that handles automatic
                          type conversion for arguments and return values. for structs, types, and enums, 
                          a ctypes class is returned matching the type specified.
        lib.name   - searches in order through values, functions, types, structs, unions, and enums from 
                     header definitions and returns an object for the first match found. The object
                     returned is the same as returned by lib(type, name). This is the preferred way to access
                     elements from CLibrary, but may not work in some situations (for example, if
                     a struct and variable share the same name).
        lib[type]  - Accesses the header definitions directly, returns definition dictionaries
                     based on the type requested. This is equivalent to headers.defs[type].
    """
    Null = object()
    
    cTypes = {
        'char': c_char,
        'wchar': c_wchar,
        'unsigned char': c_ubyte,
        'short': c_short,
        'short int': c_short,
        'unsigned short': c_ushort,
        'unsigned short int': c_ushort,
        'int': c_int,
        'unsigned': c_uint,
        'unsigned int': c_uint,
        'long': c_long,
        'long int': c_long,
        'unsigned long': c_ulong,
        'unsigned long int': c_ulong,
        '__int64': c_longlong,
        'long long': c_longlong,
        'long long int': c_longlong,
        'unsigned __int64': c_ulonglong,
        'unsigned long long': c_ulonglong,
        'unsigned long long int': c_ulonglong,
        'float': c_float,
        'double': c_double,
        'long double': c_longdouble
    }
    cPtrTypes = {
        'char': c_char_p,
        'wchar': c_wchar_p,
        'void': c_void_p
    }
        
        
    
    def __init__(self, lib, headers, prefix=None):
        ## name everything using underscores to avoid name collisions with library
        
        self._lib_ = lib
        self._headers_ = headers
        self._defs_ = headers.defs
        if prefix is None:
            self._prefix_ = []
        elif type(prefix) is list:
            self._prefix_ = prefix
        else:
            self._prefix_ = [prefix]
        self._objs_ = {}
        for k in ['values', 'functions', 'types', 'structs', 'unions', 'enums']:
            self._objs_[k] = {}
        self._allObjs_ = {}
        self._structs_ = {}
        self._unions_ = {}

    def __call__(self, typ, name):
        if typ not in self._objs_:
            typs = self._objs_.keys()
            raise Exception("Type must be one of %s" % str(typs))
        
        if name not in self._objs_[typ]:
            self._objs_[typ][name] = self._mkObj_(typ, name)
            
        return self._objs_[typ][name]

    def _allNames_(self, name):
        return [name] + [p + name for p in self._prefix_]

    def _mkObj_(self, typ, name):
        names = self._allNames_(name)
        
        for n in names:
            if n in self._objs_:
                return self._objs_[n]
            
        for n in names:  ## try with and without prefix
            if n not in self._defs_[typ] and not (typ in ['structs', 'unions', 'enums'] and n in self._defs_['types']):
                continue
                
            if typ == 'values':
                return self._defs_[typ][n]
            elif typ == 'functions':
                return self._getFunction(n)
            elif typ == 'types':
                obj = self._defs_[typ][n]
                return self._ctype(obj)
            elif typ == 'structs':
                return self._cstruct('structs', n)
            elif typ == 'unions':
                return self._cstruct('unions', n)
            elif typ == 'enums':
                ## Allow automatic resolving of typedefs that alias enums
                if n not in self._defs_['enums']:
                    if n not in self._defs_['types']:
                        raise Exception('No enums named "%s"' % n)
                    typ = self._headers_.evalType([n])[0]
                    if typ[:5] != 'enum ':
                        raise Exception('No enums named "%s"' % n)
                    n = self._defs_['types'][typ][1]  ## look up internal name of enum 
                obj = self._defs_['enums'][n]
                    
                return obj
            else:
                raise Exception("Unknown type %s" % typ)
        raise NameError(name)
        

    def __getattr__(self, name):
        """Used to retrieve any type of definition from the headers. Searches for the name in this order:
        values, functions, types, structs, unions, enums."""
        if name not in self._allObjs_:
            names = self._allNames_(name)
            for k in ['values', 'functions', 'types', 'structs', 'unions', 'enums', None]:
                if k is None:
                    #obj = getattr(self._lib_, name)  # pull directly from the lib as a last resort?
                    raise NameError(name)
                obj = None
                for n in names:
                    if n in self._defs_[k]:
                        obj = self(k, n)
                        break
                if obj is not None:
                    break
            self._allObjs_[name] = obj
        return self._allObjs_[name]

    def __getitem__(self, name):
        """Used to retrieve a specific dictionary from the headers."""
        return self._defs_[name]
        
    def __repr__(self):
        return "<CLibrary instance: %s>" % str(self._lib_)
        
    def _getFunction(self, funcName):
        try:
            func = getattr(self._lib_, funcName)
        except:
            raise Exception("Function name '%s' appears in headers but not in library!" % func)
            
        #print "create function %s," % (funcName), self._defs_['functions'][funcName]
        return CFunction(self, func, self._defs_['functions'][funcName], funcName)
        
    def _ctype(self, typ, pointers=True):
        """return a ctype object representing the named type. 
        If pointers is True, the class returned includes all pointer/array specs provided. 
        Otherwise, the class returned is just the base type with no pointers."""
        try:
            typ = self._headers_.evalType(typ)
            mods = typ[1:][:]
            
            ## Create the initial type
            ## Some types like ['char', '*'] have a specific ctype (c_char_p)
            ## (but only do this if pointers == True)
            if pointers and len(typ) > 1 and typ[1] == '*' and typ[0] in CLibrary.cPtrTypes:
                cls = CLibrary.cPtrTypes[typ[0]]
                mods = typ[2:]
                
            ## If the base type is in the list of existing ctypes:
            elif typ[0] in CLibrary.cTypes:
                cls = CLibrary.cTypes[typ[0]]
                
            ## structs, unions, enums:
            elif typ[0][:7] == 'struct ':
                cls = self._cstruct('structs', self._defs_['types'][typ[0]][1])
            elif typ[0][:6] == 'union ':
                cls = self._cstruct('unions', self._defs_['types'][typ[0]][1])
            elif typ[0][:5] == 'enum ':
                cls = c_int
                
            ## void
            elif typ[0] == 'void':
                cls = None
            else:
                #print typ
                raise Exception("Can't find base type for %s" % str(typ))
            
            if not pointers:
                return cls
                
            ## apply pointers and arrays
            while len(mods) > 0:
                m = mods.pop(0)
                if isinstance(m, basestring):  ## pointer or reference
                    if m[0] == '*' or m[0] == '&':
                        for i in m:
                            cls = POINTER(cls)
                elif type(m) is list:          ## array
                    for i in m:
                        if i == -1:            ## -1 indicates an 'incomplete type' like "int variable[]"
                            cls = POINTER(cls) ## which we should interpret like "int *variable"
                        else:
                            cls = cls * i
                elif type(m) is tuple:   ## Probably a function pointer
                    ## Find pointer and calling convention
                    isPtr = False
                    conv = '__cdecl'
                    if len(mods) == 0:
                        raise Exception("Function signature with no pointer:", m, mods)
                    for i in [0,1]:
                        if len(mods) < 1:
                            break
                        if mods[0] == '*':
                            mods.pop(0)
                            isPtr = True
                        elif mods[0] in ['__stdcall', '__cdecl']:
                            conv = mods.pop(0)
                        else:
                            break
                    if not isPtr:
                        raise Exception("Not sure how to handle type (function without single pointer): %s" % str(typ))
                            
                    if conv == '__stdcall':
                        mkfn = WINFUNCTYPE
                    else:
                        mkfn = CFUNCTYPE
                    #print "Create function pointer (%s)" % conv
                    
                    args = [self._ctype(arg[1]) for arg in m]
                    cls = mkfn(cls, *args)
                            
                else:
                    raise Exception("Not sure what to do with this type modifier: '%s'" % str(p))
            return cls
        except:
            print "Error while processing type", typ
            raise
        
    def _cstruct(self, strType, strName):
        if strName not in self._structs_:
            
            ## Resolve struct name--typedef aliases allowed.
            if strName not in self._defs_[strType]:
                if strName not in self._defs_['types']:
                    raise Exception('No struct/union named "%s"' % strName)
                typ = self._headers_.evalType([strName])[0]
                if typ[:7] != 'struct ' and typ[:6] != 'union ':
                    raise Exception('No struct/union named "%s"' % strName)
                strName = self._defs_['types'][typ][1]
                
            ## Pull struct definition
            defn = self._defs_[strType][strName]
                
                
            ## create ctypes class
            defs = defn['members'][:]
            if strType == 'structs':
                class s(Structure):
                    def __repr__(self):
                        return "<ctypes struct '%s'>" % strName
            elif strType == 'unions':
                class s(Union):
                    def __repr__(self):
                        return "<ctypes union '%s'>" % strName
            
            
            ## must register struct here to allow recursive definitions.
            self._structs_[strName] = s
            
            if defn['pack'] is not None:
                s._pack_ = defn['pack']
            
            ## assign names to anonymous members
            members = []
            anon = []
            for i in range(len(defs)):
                if defs[i][0] is None:
                    c = 0
                    while True:
                        name = 'anon_member%d' % c
                        if name not in members:
                            defs[i][0] = name
                            anon.append(name)
                            break
                members.append(defs[i][0])
            
            s._anonymous_ = anon
            s._fields_ = [(m[0], self._ctype(m[1])) for m in defs]
            s._defaults_ = [m[2] for m in defs]
        return self._structs_[strName]
        


class CFunction:
    def __init__(self, lib, func, sig, name):
        self.lib = lib
        self.func = func
        #print sig
        self.sig = list(sig) # looks like [return_type, [(argName, type, default), (argName, type, default), ...]]
        self.sig[1] = [s for s in sig[1] if s[1] != ['void']]  ## remove void args from list
        for conv in ['__stdcall', '__cdecl']:
            if conv in self.sig[0]:
                self.sig[0].remove(conv)
        self.name = name
        self.restype = lib._ctype(self.sig[0])
        #func.restype = self.restype
        self.argTypes = [lib._ctype(s[1]) for s in self.sig[1]]
        func.argtypes = self.argTypes
        self.reqArgs = [x[0] for x in self.sig[1] if x[2] is None]
        self.argInds = dict([(self.sig[1][i][0], i) for i in range(len(self.sig[1]))])  ## mapping from argument names to indices
        #print "created func", self, sig, self.argTypes

    def argCType(self, arg):
        """Return the ctype required for the specified argument.
        arg can be either an integer  or the name of the argument.
        """
        if isinstance(arg, basestring):
            arg = self.argInds[arg]
        return self.lib._ctype(self.sig[1][arg][1])
    
    def __call__(self, *args, **kwargs):
        """Invoke the SO or dll function referenced, converting all arguments to the correct type.
        Keyword arguments are allowed as long as the header specifies the argument names.
        Arguments which are passed byref may be omitted entirely, and will be automaticaly generated.
        To pass a NULL pointer, give None as the argument.
        Returns the return value of the function call as well as all of the arguments (so that objects passed by reference can be retrieved)"""
        #print "CALL: %s(%s)" % (self.name, ", ".join(map(str, args) + ["%s=%s" % (k, str(kwargs[k])) for k in kwargs]))
        #print "  sig:", self.sig
        argList = [None] * max(len(self.reqArgs), len(args))  ## We'll need at least this many arguments.
        
        ## First fill in args
        for i in range(len(args)):
            #argList[i] = self.argTypes[i](args[i])
            if args[i] is None:
                argList[i] = self.lib.Null
            else:
                argList[i] = args[i]
        
        ## Next fill in kwargs
        for k in kwargs:
            #print "    kw:", k
            if k not in self.argInds:
                print "Function signature:", self.prettySignature()
                raise Exception("Function signature has no argument named '%s'" % k)
            ind = self.argInds[k]
            if ind >= len(argList):  ## stretch argument list if needed
                argList += [None] * (ind - len(argList) + 1)
            #argList[ind] = self.coerce(kwargs[k], self.argTypes[ind])
            if kwargs[k] is None:
                argList[ind] = self.lib.Null
            else:
                argList[ind] = kwargs[k]
        
        guessedArgs = []
        ## Finally, fill in remaining arguments if they are pointers to int/float/void*/struct values 
        ## (we assume these are to be modified by the function and their initial value is not important)
        for i in range(len(argList)):
            if argList[i] is None or argList[i] is self.lib.Null:
                try:
                    sig = self.sig[1][i][1]
                    argType = self.lib._headers_.evalType(sig)
                    if argList[i] is self.lib.Null:  ## request to build a null pointer
                        if len(argType) < 2:
                            raise Exception("Can not create NULL for non-pointer argument type: %s" % str(argType))
                        argList[i] = self.lib._ctype(sig)()
                    #elif argType == ['char', '*']:  ## pass null pointer if none was specified. This is a little dangerous, but some functions will expect it.
                        #argList[i] = c_char_p()     ##  On second thought: let's just require the user to explicitly ask for a NULL pointer.
                    else:
                        if argType == ['void', '**'] or argType == ['void', '*', '*']:
                            cls = c_void_p
                        else:
                            assert len(argType) == 2 and argType[1] == '*' ## Must be 2-part type, second part must be '*'
                            cls = self.lib._ctype(sig, pointers=False)
                        argList[i] = pointer(cls(0))
                        guessedArgs.append(i)
                except:
                    if sys.exc_info()[0] is not AssertionError:
                        raise
                        #sys.excepthook(*sys.exc_info())
                    print "Function signature:", self.prettySignature()
                    raise Exception("Function call '%s' missing required argument %d '%s'. (See above for signature)" % (self.name, i, self.sig[1][i][0]))
        #print "  args:", argList
        try:
            res = self.func(*argList)
        except:
            print "Function call failed. Signature is:", self.prettySignature()
            print "Arguments:", argList
            print "Argtypes:", self.func.argtypes
            raise
        #print "  result:", res
        
        cr = CallResult(res, argList, self.sig, guessed=guessedArgs, restype=self.restype)
        return cr
    
    def prettySignature(self):
        return "%s %s(%s)" % (''.join(self.sig[0]), self.name, ', '.join(["%s %s" % ("".join(map(str, s[1])), s[0]) for s in self.sig[1]]))
    
class CallResult:
    """Class for bundling results from C function calls. Allows access to the function
    return value as well as all of the arguments, since the function call will often return
    extra values via these arguments. 
      - Original ctype objects can be accessed via result.rval or result.args
      - Python values carried by these objects can be accessed using ()
    To access values:
       - The return value: ()
       - The nth argument passed: [n]
       - The argument by name: ['name']
       - All values that were auto-generated: .auto()
       
    The class can also be used as an iterator, so that tuple unpacking is possible:
       ret, arg1, arg2 = lib.runSomeFunction(...)
       """
    def __init__(self, rval, args, sig, guessed, restype):
        self.rval = rval        ## return value of function call
        self.args = args        ## list of arguments to function call
        self.sig = sig          ## function signature
        self.guessed = guessed  ## list of arguments that were generated automatically (usually byrefs)
        self.restype = restype  ## return type specified by signature
        
    def __call__(self):
        #print "Clibrary:", type(self.rval), self.mkVal(self.rval)
        if self.sig[0] == ['void']:
            return None
        return self.mkVal(self.rval)
        
    def __getitem__(self, n):
        if type(n) is int:
            return self.mkVal(self.args[n])
        elif type(n) is str:
            ind = self.findArg(n)
            return self.mkVal(self.args[ind])
        else:
            raise Exception("Index must be int or str.")

    def __setitem__(self, n, val):
        if type(n) is int:
            self.args[n] = val
        elif type(n) is str:
            ind = self.findArg(n)
            self.args[ind] = val
        else:
            raise Exception("Index must be int or str.")
        

    def mkVal(self, obj):
        while not hasattr(obj, 'value'):
            if not hasattr(obj, 'contents'):
                return obj
            try:
                obj = obj.contents
            except ValueError:
                return None
        
        return obj.value
        
        
    def findArg(self, arg):
        for i in range(len(self.sig[1])):
            if self.sig[1][i][0] == arg:
                return i
        raise Exception("Can't find argument '%s' in function signature. Arguments are: %s" % (arg, str([a[0] for a in self.sig[1]])))
    
    def __iter__(self):
        yield self()
        for i in range(len(self.args)):
            yield(self[i])
        
    def auto(self):
        """Return auto-filled values"""
        return [self[n] for n in self.guessed]
            
    def cast(self):
        return cast(self(), self.restype)
    
    

