"""
Register external C functions necessary for Numba code generation.
"""

import sys
import ctypes

from llvmlite import ir
import llvmlite.binding as ll

from numba import llvmthreadsafe as llvmts
from numba import utils, config
from numba import _helperlib
from . import intrinsics

# Require workaround for https://support.microsoft.com/en-us/kb/982107 ?
need_kb982107 = (config.PYVERSION == (2, 7) and
                 config.IS_WIN32 and
                 not config.IS_32BITS)


def _add_missing_symbol(symbol, addr):
    """Add missing symbol into LLVM internal symtab
    """
    if not ll.address_of_symbol(symbol):
        ll.add_symbol(symbol, addr)


def _get_msvcrt_symbol(symbol):
    """
    Under Windows, look up a symbol inside the C runtime
    and return the raw pointer value as an integer.
    """
    from ctypes import cdll, cast, c_void_p
    f = getattr(cdll.msvcrt, symbol)
    return cast(f, c_void_p).value


def compile_multi3(context):
    """
    Compile the multi3() helper function used by LLVM
    for 128-bit multiplication on 32-bit platforms.
    """
    codegen = context.codegen()
    library = codegen.create_library("multi3")

    ir_mod = library.create_ir_module("multi3")

    i64 = ir.IntType(64)
    i128 = ir.IntType(128)
    lower_mask = ir.Constant(i64, 0xffffffff)
    _32 = ir.Constant(i64, 32)
    _64 = ir.Constant(i128, 64)

    fn_type = ir.FunctionType(i128, [i128, i128])
    fn = ir.Function(ir_mod, fn_type, name="multi3")

    a, b = fn.args
    bb = fn.append_basic_block()
    builder = ir.IRBuilder(bb)

    # This implementation mimicks compiler-rt's.
    al = builder.trunc(a, i64)
    bl = builder.trunc(b, i64)
    ah = builder.trunc(builder.ashr(a, _64), i64)
    bh = builder.trunc(builder.ashr(b, _64), i64)

    # Compute {rh, rl} = al * bl   (unsigned 64-bit multiplication)
    # rl = (al & 0xffffffff) * (bl & 0xffffffff)
    rl = builder.mul(builder.and_(al, lower_mask), builder.and_(bl, lower_mask))
    # t = rl >> 32
    t = builder.lshr(rl, _32)
    # rl &= 0xffffffff
    rl = builder.and_(rl, lower_mask)
    # t += (al >> 32) * (bl & 0xffffffff)
    t = builder.add(t, builder.mul(builder.lshr(al, _32),
                                   builder.and_(bl, lower_mask)))
    # rl += t << 32
    rl = builder.add(rl, builder.shl(t, _32))
    # rh = t >> 32
    rh = builder.lshr(t, _32)
    # t = rl >> 32
    t = builder.lshr(rl, _32)
    # rl &= 0xffffffff
    rl = builder.and_(rl, lower_mask)
    # t += (bl >> 32) * (al & 0xffffffff)
    t = builder.add(t, builder.mul(builder.lshr(bl, _32),
                                   builder.and_(al, lower_mask)))
    # rl += t << 32
    rl = builder.add(rl, builder.shl(t, _32))
    # rh += t >> 32
    rh = builder.add(rh, builder.lshr(t, _32))
    # rh += (al >> 32) * (bl >> 32)
    rh = builder.add(rh, builder.mul(builder.lshr(al, _32),
                                     builder.lshr(bl, _32)))

    # rh += (bh * al) + (bl * ah)
    rh = builder.add(rh, builder.mul(bh, al))
    rh = builder.add(rh, builder.mul(bl, ah))

    # r = rl + (rh << 64)
    r = builder.zext(rl, i128)
    r = builder.add(r, builder.shl(builder.zext(rh, i128), _64))
    builder.ret(r)

    library.add_ir_module(ir_mod)
    library.finalize()

    return library


class _Installer(object):

    _installed = False

    def install(self, context):
        """
        Install the functions into LLVM.  This only needs to be done once,
        as the mappings are persistent during the process lifetime.
        """
        if not self._installed:
            self._do_install(context)
            self._installed = True


class _ExternalMathFunctions(_Installer):
    """
    Map the math functions from the C runtime library into the LLVM
    execution environment.
    """

    def _do_install(self, context):
        is32bit = utils.MACHINE_BITS == 32
        c_helpers = _helperlib.c_helpers

        if sys.platform.startswith('win32') and is32bit:
            # For Windows XP _ftol2 is not defined, we will just use
            # _ftol as a replacement.
            # On Windows 7, this is not necessary but will work anyway.
            ftol = _get_msvcrt_symbol("_ftol")
            _add_missing_symbol("_ftol2", ftol)

        elif sys.platform.startswith('linux') and is32bit:
            _add_missing_symbol("__fixunsdfdi", c_helpers["fptoui"])
            _add_missing_symbol("__fixunssfdi", c_helpers["fptouif"])

        if is32bit:
            # Make the library immortal
            self._multi3_lib = compile_multi3(context)
            ptr = self._multi3_lib.get_pointer_to_function("multi3")
            assert ptr
            _add_missing_symbol("__multi3", ptr)

        # List available C-math
        for fname in intrinsics.INTR_MATH:
            # Force binding from CPython's C runtime library.
            # (under Windows, different versions of the C runtime can
            #  be loaded at the same time, for example msvcrt100 by
            #  CPython and msvcrt120 by LLVM)
            if need_kb982107 and fname.startswith('fmod'):
                ll.add_symbol(fname, c_helpers['fixed_' + fname])
            else:
                ll.add_symbol(fname, c_helpers[fname])

        if need_kb982107:
            # Make the library immortal
            self._kb982107_lib = set_fnclex(context, c_helpers)


def set_fnclex(context, c_helpers):
    """
    Install fnclex before fmod calls.
    Workaround for https://support.microsoft.com/en-us/kb/982107
    """
    ptr_set_fnclex = c_helpers['set_fnclex']
    fn = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(ptr_set_fnclex)

    library = compile_fnclex(context)
    fnclex_ptr = library.get_pointer_to_function('fnclex')
    fn(fnclex_ptr)

    return library


def compile_fnclex(context):
    """
    Compile a function that calls fnclex to workround
    https://support.microsoft.com/en-us/kb/982107
    """
    codegen = context.codegen()
    library = codegen.create_library("kb982107")
    ir_mod = """
define void @fnclex() {
  call void asm sideeffect "fnclex", ""()
  ret void
}
    """
    ll.initialize_native_asmparser()
    library.add_llvm_module(llvmts.parse_assembly(ir_mod))
    library.finalize()
    return library


c_math_functions = _ExternalMathFunctions()
