# ______________________________________________________________________
from __future__ import absolute_import
import sys
import os.path
import imp
import io
import types

import llvm.core as lc
import llvm.ee as le

from . import bytetype, byte_translator
from .pyaddfunc import pyaddfunc

LLVM_TO_INT_PARSE_STR_MAP = {
    8 : 'b',
    16 : 'h',
    32 : 'i', # Note that on 32-bit systems sizeof(int) == sizeof(long)
    64 : 'L', # Seeing sizeof(long long) == 8 on both 32 and 64-bit platforms
}

LLVM_TO_PARSE_STR_MAP = {
    lc.TYPE_FLOAT : 'f',
    lc.TYPE_DOUBLE : 'd',
}

# ______________________________________________________________________

# XXX Stolen from numba.translate

def get_string_constant (module, const_str):
    const_name = "__STR_%x" % (hash(const_str),)
    try:
        ret_val = module.get_global_variable_named(const_name)
    except:
        lconst_str = lc.Constant.stringz(const_str)
        ret_val = module.add_global_variable(lconst_str.type, const_name)
        ret_val.initializer = lconst_str
        ret_val.linkage = lc.LINKAGE_INTERNAL
    return ret_val

# ______________________________________________________________________

class NoBitey (object):
    def __init__ (self, target_module = None, type_annotations = None):
        if target_module is None:
            target_module = lc.Module.new('NoBitey_%d' % id(self))
        if type_annotations is None:
            type_annotations = {}
        self.target_module = target_module
        self.type_aliases = type_annotations # Reserved for future use.

    def _build_parse_string (self, llvm_type):
        kind = llvm_type.kind
        if kind == lc.TYPE_INTEGER:
            ret_val = LLVM_TO_INT_PARSE_STR_MAP[llvm_type.width]
        elif kind in LLVM_TO_PARSE_STR_MAP:
            ret_val = LLVM_TO_PARSE_STR_MAP[kind]
        else:
            raise TypeError('Unsupported LLVM type: %s' % str(llvm_type))
        return ret_val

    def build_parse_string (self, llvm_tys):
        """Given a set of LLVM types, return a string for parsing
        them via PyArg_ParseTuple."""
        return ''.join((self._build_parse_string(ty)
                        for ty in llvm_tys))

    def handle_abi_casts (self, builder, result):
        if result.type.kind == lc.TYPE_FLOAT:
            # NOTE: The C ABI apparently casts floats to doubles when
            # an argument must be pushed on the stack, as is the case
            # when calling a variable argument function.
            # XXX Is there documentation on this where I can find all
            # coercion rules?  Do we still need some libffi
            # integration?
            result = builder.fpext(result, bytetype.ldouble)
        return result

    def build_wrapper_function (self, llvm_function, engine = None):
        arg_types = llvm_function.type.pointee.args
        return_type = llvm_function.type.pointee.return_type
        li32_0 = lc.Constant.int(bytetype.li32, 0)
        def get_llvm_function (builder):
            if self.target_module != llvm_function.module:
                llvm_function_ptr = self.target_module.add_global_variable(
                    llvm_function.type, llvm_function.name)
                llvm_function_ptr.initializer = lc.Constant.inttoptr(
                    lc.Constant.int(
                        bytetype.liptr,
                        engine.get_pointer_to_function(llvm_function)),
                    llvm_function.type)
                llvm_function_ptr.linkage = lc.LINKAGE_INTERNAL
                ret_val = builder.load(llvm_function_ptr)
            else:
                ret_val = llvm_function
            return ret_val
        def build_parse_args (builder):
            return [builder.alloca(arg_type) for arg_type in arg_types]
        def build_parse_string (builder):
            parse_str = get_string_constant(
                self.target_module, self.build_parse_string(arg_types))
            return builder.gep(parse_str, (li32_0, li32_0))
        def load_target_args (builder, args):
            return [builder.load(arg) for arg in args]
        def build_build_string (builder):
            build_str = get_string_constant(
                self.target_module, self._build_parse_string(return_type))
            return builder.gep(build_str, (li32_0, li32_0))
        handle_abi_casts = self.handle_abi_casts
        target_function_name = llvm_function.name + "_wrapper"
        # __________________________________________________
        @byte_translator.llpython(bytetype.l_pyfunc, self.target_module,
                                   **locals())
        def _wrapper (self, args):
            ret_val = l_pyobj_p(0)
            parse_args = build_parse_args()
            parse_result = PyArg_ParseTuple(args, build_parse_string(),
                                            *parse_args)
            if parse_result != li32(0):
                thread_state = PyEval_SaveThread()
                target_args = load_target_args(parse_args)
                llresult = handle_abi_casts(get_llvm_function()(*target_args))
                PyEval_RestoreThread(thread_state)
                ret_val = Py_BuildValue(build_build_string(), llresult)
            return ret_val
        # __________________________________________________
        return _wrapper

    def wrap_llvm_module (self, llvm_module, engine = None, py_module = None):
        '''
        Shamefully adapted from bitey.bind.wrap_llvm_module().
        '''
        functions = [func for func in llvm_module.functions
                     if not func.name.startswith("_")
                     and not func.is_declaration
                     and func.linkage == lc.LINKAGE_EXTERNAL]
        if engine is None:
            engine = le.ExecutionEngine.new(llvm_module)
        wrappers = [self.build_wrapper_function(func, engine)
                    for func in functions]
        if __debug__: print(self.target_module)
        if self.target_module != llvm_module:
            engine.add_module(self.target_module)
        py_wrappers = [pyaddfunc(wrapper.name,
                                 engine.get_pointer_to_function(wrapper))
                       for wrapper in wrappers]
        if py_module:
            for py_wrapper in py_wrappers:
                setattr(py_module, py_wrapper.__name__[:-8], py_wrapper)
            setattr(py_module, '_llvm_module', llvm_module)
            setattr(py_module, '_llvm_engine', engine)
            if self.target_module != llvm_module:
                setattr(py_module, '_llvm_wrappers', self.target_module)
        return engine, py_wrappers

    def wrap_llvm_module_in_python (self, llvm_module, py_module = None):
        '''
        Mildly reworked and abstracted bitey.bind.wrap_llvm_bitcode().
        Abstracted to accept any existing LLVM Module object, and
        return a Python wrapper module (even if one wasn't originally
        specified).
        '''
        if py_module is None:
            py_module = types.ModuleType(str(llvm_module.id))
        engine = le.ExecutionEngine.new(llvm_module)
        self.wrap_llvm_module(llvm_module, engine, py_module)
        return py_module

    def wrap_llvm_bitcode (self, bitcode, py_module = None):
        '''
        Intended to be drop-in replacement of
        bitey.bind.wrap_llvm_bitcode().
        '''
        return self.wrap_llvm_module_in_python(
            lc.Module.from_bitcode(io.BytesIO(bitcode)), py_module)

    def wrap_llvm_assembly (self, llvm_asm, py_module = None):
        return self.wrap_llvm_module_in_python(
            lc.Module.from_assembly(io.BytesIO(llvm_asm)), py_module)

# ______________________________________________________________________

class NoBiteyLoader(object):
    """
    Load LLVM compiled bitcode and autogenerate a ctypes binding.

    Initially copied and adapted from bitey.loader module.
    """
    def __init__(self, pkg, name, source, preload, postload):
        self.package = pkg
        self.name = name
        self.fullname = '.'.join((pkg,name))
        self.source = source
        self.preload = preload
        self.postload = postload

    @classmethod
    def _check_magic(cls, filename):
        if os.path.exists(filename):
            magic = open(filename,"rb").read(4)
            if magic == b'\xde\xc0\x17\x0b':
                return True
            elif magic[:2] == b'\x42\x43':
                return True
            else:
                return False
        else:
            return False

    @classmethod
    def build_module(cls, fullname, source_path, source_data, preload=None,
                     postload=None):
        name = fullname.split(".")[-1]
        mod = imp.new_module(name)
        if preload:
            exec(preload, mod.__dict__, mod.__dict__)
        type_annotations = getattr(mod, '_type_annotations', None)
        nb = NoBitey(type_annotations = type_annotations)
        if source_path.endswith(('.o', '.bc')):
            nb.wrap_llvm_bitcode(source_data, mod)
        elif source_path.endswith('.s'):
            nb.wrap_llvm_assembly(source_data, mod)
        if postload:
            exec(postload, mod.__dict__, mod.__dict__)
        return mod

    @classmethod
    def find_module(cls, fullname, paths = None):
        if paths is None:
            paths = sys.path
        names = fullname.split('.')
        modname = names[-1]
        source_paths = None
        for f in paths:
            path = os.path.join(os.path.realpath(f), modname)
            source = path + '.o'
            if cls._check_magic(source):
                source_paths = path, source
                break
            source = path + '.bc'
            if os.path.exists(source):
                source_paths = path, source
                break
            source = path + '.s'
            if os.path.exists(source):
                source_paths = path, source
                break
        if source_paths:
            path, source = source_paths
            return cls('.'.join(names[:-1]), modname, source,
                       path + ".pre.py", path + ".post.py")

    def get_code(self, module):
        pass

    def get_data(self, module):
        pass

    def get_filename(self, name):
        return self.source

    def get_source(self, name):
        with open(self.source, 'rb') as f:
             return f.read()

    def is_package(self, *args, **kw):
        return False

    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]

        preload = None
        postload = None

        # Get the preload file (if any)
        if os.path.exists(self.preload):
            with open(self.preload) as f:
                preload = f.read()

        # Get the source
        with open(self.source, 'rb') as f:
            source_data = f.read()

        # Get the postload file (if any)
        if os.path.exists(self.postload):
            with open(self.postload) as f:
                postload = f.read()

        mod = self.build_module(fullname, self.get_filename(None), source_data,
                                preload, postload)
        sys.modules[fullname] = mod
        mod.__loader__ = self
        mod.__file__ = self.source
        return mod

    @classmethod
    def install(cls):
        if cls not in sys.meta_path:
            sys.meta_path.append(cls)

    @classmethod
    def remove(cls):
        sys.meta_path.remove(cls)

# ______________________________________________________________________

def _mk_add_42 (llvm_module, at_type = bytetype.lc_long):
    f = llvm_module.add_function(
        lc.Type.function(at_type, [at_type]), 'add_42_%s' % str(at_type))
    block = f.append_basic_block('entry')
    builder = lc.Builder.new(block)
    if at_type.kind == lc.TYPE_INTEGER:
        const_42 = lc.Constant.int(at_type, 42)
        add = builder.add
    elif at_type.kind in (lc.TYPE_FLOAT, lc.TYPE_DOUBLE):
        const_42 = lc.Constant.real(at_type, 42.)
        add = builder.fadd
    else:
        raise TypeError('Unsupported type: %s' % str(at_type))
    builder.ret(add(f.args[0], const_42))
    return f

# ______________________________________________________________________

def build_test_module ():
    llvm_module = lc.Module.new('nobitey_test')
    for ty in (bytetype.li32, bytetype.li64, bytetype.lfloat,
               bytetype.ldouble):
        fn = _mk_add_42(llvm_module, ty)
    return llvm_module

# ______________________________________________________________________

def test_wrap_module (arg = None):
    # Build up a module.
    m = build_test_module()
    if arg and arg.lower() == 'separated':
        wrap_module = NoBitey().wrap_llvm_module_in_python(m)
    else:
        wrap_module = NoBitey(m).wrap_llvm_module_in_python(m)
    # Now try running the generated wrappers.
    for py_wf_name in ('add_42_i32', 'add_42_i64', 'add_42_float',
                       'add_42_double'):
        py_wf = getattr(wrap_module, py_wf_name)
        for i in range(42):
            result = py_wf(i)
            expected = i + 42
            assert result == expected, "%r != %r in %r" % (
                result, expected, py_wf)
    return wrap_module

# ______________________________________________________________________

def main (*args):
    if args:
        for arg in args:
            test_wrap_module(arg)
    else:
        test_wrap_module()

if __name__ == "__main__":
    main(*sys.argv[1:])

# ______________________________________________________________________
# End of nobitey.py
