# Author: Pearu Peterson
# Created: February 2019

import re
from llvmlite import ir
import llvmlite.binding as llvm
from .targetinfo import TargetInfo
from .npy_mathimpl import *  # noqa: F403, F401
from .utils import get_version
if get_version('numba') >= (0, 49):
    from numba.core import codegen, cpu, compiler_lock, \
        registry, typing, compiler, sigutils, cgutils, \
        extending
    from numba.core import types as nb_types
else:
    from numba.targets import codegen, cpu, registry
    from numba import compiler_lock, typing, compiler, \
        sigutils, cgutils, extending
    from numba import types as nb_types

int32_t = ir.IntType(32)


def _lf(lst):
    return lst + [e + 'f' for e in lst] + [e + 'l' for e in lst]


exp_funcs = ['exp', 'exp2', 'expm1', 'log', 'log2', 'log10',
             'log1p', 'ilogb', 'logb']
power_funcs = ['sqrt', 'cbrt', 'hypot', 'pow']
trigonometric_funcs = ['sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'atan2']
hyperbolic_funcs = ['sinh', 'cosh', 'tanh', 'asinh', 'acosh', 'atanh']
nearest_funcs = ['ceil', 'floor', 'trunc', 'round', 'lround', 'llround',
                 'nearbyint', 'rint', 'lrint', 'llrint']
fp_funcs = ['frexp', 'ldexp', 'modf', 'scalbn', 'scalbln', 'nextafter',
            'nexttoward']
classification_funcs = ['fpclassify', 'isfinite', 'isinf', 'isnan',
                        'isnormal', 'signbit']

fp_funcs = _lf([*exp_funcs, *power_funcs, *trigonometric_funcs,
                *hyperbolic_funcs, *nearest_funcs, *fp_funcs])
libm_funcs = [*fp_funcs, *classification_funcs]

stdio_funcs = ['printf', 'puts']

stdlib_funcs = ['free']


def get_function_dependencies(module, funcname, _deps=None):
    if _deps is None:
        _deps = dict()
    func = module.get_function(funcname)
    assert func.name == funcname, (func.name, funcname)
    for block in func.blocks:
        for instruction in block.instructions:
            if instruction.opcode == 'call':
                name = list(instruction.operands)[-1].name
                f = module.get_function(name)
                if name.startswith('llvm.'):
                    _deps[name] = 'intrinsic'
                elif f.is_declaration:
                    if name in libm_funcs:
                        _deps[name] = 'libm'
                    elif name in stdio_funcs:
                        _deps[name] = 'stdio'
                    elif name in stdlib_funcs:
                        _deps[name] = 'stdlib'
                    elif name in ['allocate_varlen_buffer']:
                        _deps[name] = 'omnisci_internal'
                    else:
                        _deps[name] = 'undefined'
                else:
                    if name not in _deps:
                        _deps[name] = 'defined'
                        get_function_dependencies(module, name, _deps=_deps)
    return _deps


class JITRemoteCPUCodegen(codegen.JITCPUCodegen):
    # TODO: introduce JITRemoteCodeLibrary?
    _library_class = codegen.JITCodeLibrary

    def __init__(self, name, target_info):
        self.target_info = target_info
        super(JITRemoteCPUCodegen, self).__init__(name)

    def _get_host_cpu_name(self):
        return self.target_info.device_name

    def _get_host_cpu_features(self):
        features = self.target_info.device_features
        if llvm.llvm_version_info[0] < 9:
            # See https://github.com/xnd-project/rbc/issues/45
            for f in ['cx8', 'enqcmd', 'avx512bf16']:
                features = features.replace('+' + f, '').replace('-' + f, '')
        return features

    def _customize_tm_options(self, options):
        super(JITRemoteCPUCodegen, self)._customize_tm_options(options)
        # fix reloc_model as the base method sets it using local target
        if self.target_info.arch.startswith('x86'):
            reloc_model = 'static'
        else:
            reloc_model = 'default'
        options['reloc'] = reloc_model


class RemoteCPUContext(cpu.CPUContext):

    def __init__(self, typing_context, target_info):
        self.target_info = target_info
        super(RemoteCPUContext, self).__init__(typing_context)

    @compiler_lock.global_compiler_lock
    def init(self):
        self.address_size = self.target_info.bits
        self.is32bit = (self.address_size == 32)
        self._internal_codegen = JITRemoteCPUCodegen("numba.exec",
                                                     self.target_info)

        # Map external C functions.
        # nb.core.externals.c_math_functions.install(self)
        # TODO, seems problematic only for 32bit cases

        # Initialize NRT runtime
        # nb.core.argets.cpu.rtsys.initialize(self)
        # TODO: is this needed for LLVM IR generation?

        # import numba.unicode  # unicode support is not relevant here

    # TODO: overwrite load_additional_registries, call_conv?, post_lowering


def make_wrapper(fname, atypes, rtype, cres):
    """Make wrapper function to numba compile result.

    The compilation result contains a function with prototype::

      <status> <function name>(<rtype>** result, <arguments>)

    The make_wrapper adds a wrapper function to compilation result
    library with the following prototype::

      <rtype> <fname>(<arguments>)

    or, if <rtype> is Omnisci Array, then

      void <fname>(<rtype>* <arguments>)

    Parameters
    ----------
    fname : str
      Function name.
    atypes : tuple
      A tuple of argument Numba types.
    rtype : numba.Type
      The return Numba type
    cres : CompileResult
      Numba compilation result.

    """
    fndesc = cres.fndesc
    module = cres.library.create_ir_module(fndesc.unique_name)
    context = cres.target_context
    ll_argtypes = [context.get_value_type(ty) for ty in atypes]
    ll_return_type = context.get_value_type(rtype)

    # TODO: design a API for custom wrapping
    if type(rtype).__name__ == 'ArrayPointer':
        wrapty = ir.FunctionType(ir.VoidType(),
                                 [ll_return_type] + ll_argtypes)
        wrapfn = module.add_function(wrapty, fname)
        builder = ir.IRBuilder(wrapfn.append_basic_block('entry'))
        fnty = context.call_conv.get_function_type(rtype, atypes)
        fn = builder.module.add_function(fnty, cres.fndesc.llvm_func_name)
        status, out = context.call_conv.call_function(
            builder, fn, rtype, atypes, wrapfn.args[1:])
        with cgutils.if_unlikely(builder, status.is_error):
            cgutils.printf(builder,
                           f"rbc: {fname} failed with status code %i\n",
                           status.code)
            builder.ret_void()
        builder.store(builder.load(out), wrapfn.args[0])
        builder.ret_void()
    else:
        wrapty = ir.FunctionType(ll_return_type, ll_argtypes)
        wrapfn = module.add_function(wrapty, fname)
        builder = ir.IRBuilder(wrapfn.append_basic_block('entry'))
        fnty = context.call_conv.get_function_type(rtype, atypes)
        fn = builder.module.add_function(fnty, cres.fndesc.llvm_func_name)
        status, out = context.call_conv.call_function(
            builder, fn, rtype, atypes, wrapfn.args)
        with cgutils.if_unlikely(builder, status.is_error):
            cgutils.printf(builder,
                           f"rbc: {fname} failed with status code %i\n",
                           status.code)
        builder.ret(out)

    cres.library.add_ir_module(module)


def compile_to_LLVM(functions_and_signatures, target: TargetInfo,
                    pipeline_class=compiler.Compiler, debug=False):
    """Compile functions with given signatures to target specific LLVM IR.

    Parameters
    ----------
    functions_and_signatures : list
      Specify a list of Python function and its signatures pairs.
    target : TargetInfo
      Specify target device information.
    debug : bool

    Returns
    -------
    module : llvmlite.binding.ModuleRef
      LLVM module instance. To get the IR string, use `str(module)`.

    """
    target_desc = registry.cpu_target
    if target is None:
        target = TargetInfo.host()
        typing_context = target_desc.typing_context
        target_context = target_desc.target_context
    else:
        typing_context = typing.Context()
        target_context = RemoteCPUContext(typing_context, target)
        # Bring over Array overloads (a hack):
        target_context._defns = target_desc.target_context._defns

    typing_context.target_info = target
    target_context.target_info = target

    codegen = target_context.codegen()
    main_library = codegen.create_library('rbc.irtools.compile_to_IR')
    main_module = main_library._final_module

    flags = compiler.Flags()
    flags.set('no_compile')
    flags.set('no_cpython_wrapper')

    function_names = []
    for func, signatures in functions_and_signatures:
        for sig in signatures:
            fname = func.__name__ + sig.mangling
            function_names.append(fname)
            args, return_type = sigutils.normalize_signature(
                sig.tonumba(bool_is_int8=True))
            cres = compiler.compile_extra(typingctx=typing_context,
                                          targetctx=target_context,
                                          func=func,
                                          args=args,
                                          return_type=return_type,
                                          flags=flags,
                                          library=main_library,
                                          locals={},
                                          pipeline_class=pipeline_class)
            make_wrapper(fname, args, return_type, cres)

    seen = set()
    for _library in main_library._linking_libraries:
        if _library not in seen:
            seen.add(_library)
            main_module.link_in(
                _library._get_module_for_linking(), preserve=True,
            )

    main_library._optimize_final_module()

    # Catch undefined functions:
    used_functions = set(function_names)
    for fname in function_names:
        deps = get_function_dependencies(main_module, fname)
        for fn, descr in deps.items():
            used_functions.add(fn)
            if descr == 'undefined':
                if fn.startswith('numba_') and target.has_numba:
                    continue
                if fn.startswith('Py') and target.has_cpython:
                    continue
                raise RuntimeError('function `%s` is undefined' % (fn))

    # for global_variable in main_module.global_variables:
    #    global_variable.linkage = llvm.Linkage.private

    unused_functions = [f.name for f in main_module.functions
                        if f.name not in used_functions]

    if debug:
        print('compile_to_IR: the following functions are used')
        for fname in used_functions:
            lf = main_module.get_function(fname)
            print('  [ALIVE]', fname, 'with', lf.linkage)

    if unused_functions:
        if debug:
            print('compile_to_IR: the following functions are not used'
                  ' and will be removed:')
        for fname in unused_functions:
            lf = main_module.get_function(fname)
            if lf.is_declaration:
                # if the function is a declaration,
                # we just put the linkage as external
                lf.linkage = llvm.Linkage.external
            else:
                # but if the function is not a declaration,
                # we change the linkage to private
                lf.linkage = llvm.Linkage.private
            if debug:
                print('  [DEAD]', fname, 'with', lf.linkage)

        main_library._optimize_final_module()
    # TODO: determine unused global_variables and struct_types

    main_module.verify()
    main_library._finalized = True

    main_module.triple = target.triple
    main_module.data_layout = target.datalayout

    return main_module


def compile_IR(ir):
    """Return execution engine with IR compiled in.

    Parameters
    ----------
    ir : str
      Specify LLVM IR code as a string.

    Returns
    -------
    engine :
      Execution engine.


    Examples
    --------

        To get the address of the compiled functions, use::

          addr = engine.get_function_address("<function name>")
    """
    triple = re.search(
        r'target\s+triple\s*=\s*"(?P<triple>[-\d\w\W_]+)"\s*$',
        ir, re.M).group('triple')

    # Create execution engine
    target = llvm.Target.from_triple(triple)
    target_machine = target.create_target_machine()
    backing_mod = llvm.parse_assembly("")
    engine = llvm.create_mcjit_compiler(backing_mod, target_machine)

    # Create LLVM module and compile
    mod = llvm.parse_assembly(ir)
    mod.verify()
    engine.add_module(mod)
    engine.finalize_object()
    engine.run_static_constructors()

    return engine


@extending.intrinsic
def printf(typingctx, format_type, *args):
    """printf that can be called from Numba jit-decorated functions.
    """
    if isinstance(format_type, nb_types.StringLiteral):
        sig = nb_types.void(format_type, nb_types.BaseTuple.from_types(args))

        def codegen(context, builder, signature, args):
            cgutils.printf(builder, format_type.literal_value, *args[1:])

        return sig, codegen
