"""
Implements helpers to build LLVM debuginfo.
"""


import abc
import os.path

from llvmlite import ir

from numba.core.utils import add_metaclass


@add_metaclass(abc.ABCMeta)
class AbstractDIBuilder(object):
    @abc.abstractmethod
    def mark_variable(self, builder, allocavalue, name, lltype, size, loc):
        """Emit debug info for the variable.
        """
        pass

    @abc.abstractmethod
    def mark_location(self, builder, loc):
        """Emit source location information to the given IRBuilder.
        """
        pass

    @abc.abstractmethod
    def mark_subprogram(self, function, name, loc):
        """Emit source location information for the given function.
        """
        pass

    @abc.abstractmethod
    def finalize(self):
        """Finalize the debuginfo by emitting all necessary metadata.
        """
        pass


class DummyDIBuilder(AbstractDIBuilder):

    def __init__(self, module, filepath):
        pass

    def mark_variable(self, builder, allocavalue, name, lltype, size, loc):
        pass

    def mark_location(self, builder, loc):
        pass

    def mark_subprogram(self, function, name, loc):
        pass

    def finalize(self):
        pass


class DIBuilder(AbstractDIBuilder):
    DWARF_VERSION = 4
    DEBUG_INFO_VERSION = 3
    DBG_CU_NAME = 'llvm.dbg.cu'

    def __init__(self, module, filepath):
        self.module = module
        self.filepath = os.path.abspath(filepath)
        self.difile = self._di_file()
        self.subprograms = []
        self.dicompileunit = self._di_compile_unit()

    def _var_type(self, lltype, size):
        m = self.module
        bitsize = size * 8

        int_type = ir.IntType,
        real_type = ir.FloatType, ir.DoubleType
        # For simple numeric types, choose the closest encoding.
        # We treat all integers as unsigned.
        if isinstance(lltype, int_type + real_type):
            mdtype = m.add_debug_info('DIBasicType', {
                'name': str(lltype),
                'size': bitsize,
                'encoding': (ir.DIToken('DW_ATE_unsigned')
                             if isinstance(lltype, int_type)
                             else ir.DIToken('DW_ATE_float')),
            })
        # For all other types, describe it as sequence of bytes
        else:
            count = size
            mdrange = m.add_debug_info('DISubrange', {
                'count': count,
            })
            mdbase = m.add_debug_info('DIBasicType', {
                'name': 'byte',
                'size': 8,
                'encoding': ir.DIToken('DW_ATE_unsigned_char'),
            })
            mdtype = m.add_debug_info('DICompositeType', {
                'tag': ir.DIToken('DW_TAG_array_type'),
                'baseType': mdbase,
                'name': str(lltype),
                'size': bitsize,
                'identifier': str(lltype),
                'elements': m.add_metadata([mdrange]),
            })
        return mdtype

    def mark_variable(self, builder, allocavalue, name, lltype, size, loc):
        m = self.module
        fnty = ir.FunctionType(ir.VoidType(), [ir.MetaDataType()] * 3)
        decl = m.get_or_insert_function(fnty, name='llvm.dbg.declare')

        mdtype = self._var_type(lltype, size)
        name = name.replace('.', '$')    # for gdb to work correctly
        mdlocalvar = m.add_debug_info('DILocalVariable', {
            'name': name,
            'arg': 0,
            'scope': self.subprograms[-1],
            'file': self.difile,
            'line': loc.line,
            'type': mdtype,
        })
        mdexpr = m.add_debug_info('DIExpression', {})

        return builder.call(decl, [allocavalue, mdlocalvar, mdexpr])

    def mark_location(self, builder, loc):
        builder.debug_metadata = self._add_location(loc.line)

    def mark_subprogram(self, function, name, loc):
        di_subp = self._add_subprogram(name=name, linkagename=function.name,
                                       line=loc.line)
        function.set_metadata("dbg", di_subp)
        # disable inlining for this function for easier debugging
        function.attributes.add('noinline')

    def finalize(self):
        dbgcu = self.module.get_or_insert_named_metadata(self.DBG_CU_NAME)
        dbgcu.add(self.dicompileunit)
        self._set_module_flags()

    #
    # Internal APIs
    #

    def _set_module_flags(self):
        """Set the module flags metadata
        """
        module = self.module
        mflags = module.get_or_insert_named_metadata('llvm.module.flags')
        # Set *require* behavior to warning
        # See http://llvm.org/docs/LangRef.html#module-flags-metadata
        require_warning_behavior = self._const_int(2)
        if self.DWARF_VERSION is not None:
            dwarf_version = module.add_metadata([
                require_warning_behavior,
                "Dwarf Version",
                self._const_int(self.DWARF_VERSION)
            ])
            if dwarf_version not in mflags.operands:
                mflags.add(dwarf_version)
        debuginfo_version = module.add_metadata([
            require_warning_behavior,
            "Debug Info Version",
            self._const_int(self.DEBUG_INFO_VERSION)
        ])
        if debuginfo_version not in mflags.operands:
            mflags.add(debuginfo_version)

    def _add_subprogram(self, name, linkagename, line):
        """Emit subprogram metadata
        """
        subp = self._di_subprogram(name, linkagename, line)
        self.subprograms.append(subp)
        return subp

    def _add_location(self, line):
        """Emit location metatdaa
        """
        loc = self._di_location(line)
        return loc

    @classmethod
    def _const_int(cls, num, bits=32):
        """Util to create constant int in metadata
        """
        return ir.IntType(bits)(num)

    @classmethod
    def _const_bool(cls, boolean):
        """Util to create constant boolean in metadata
        """
        return ir.IntType(1)(boolean)

    #
    # Helpers to emit the metadata nodes
    #

    def _di_file(self):
        return self.module.add_debug_info('DIFile', {
            'directory': os.path.dirname(self.filepath),
            'filename': os.path.basename(self.filepath),
        })

    def _di_compile_unit(self):
        return self.module.add_debug_info('DICompileUnit', {
            'language': ir.DIToken('DW_LANG_Python'),
            'file': self.difile,
            'producer': 'Numba',
            'runtimeVersion': 0,
            'isOptimized': True,
            'emissionKind': 1,  # 0-NoDebug, 1-FullDebug
        }, is_distinct=True)

    def _di_subroutine_type(self):
        return self.module.add_debug_info('DISubroutineType', {
            'types': self.module.add_metadata([]),
        })

    def _di_subprogram(self, name, linkagename, line):
        return self.module.add_debug_info('DISubprogram', {
            'name': name,
            'linkageName': linkagename,
            'scope': self.difile,
            'file': self.difile,
            'line': line,
            'type': self._di_subroutine_type(),
            'isLocal': False,
            'isDefinition': True,
            'scopeLine': line,
            'isOptimized': True,
            'unit': self.dicompileunit,
        }, is_distinct=True)

    def _di_location(self, line):
        return self.module.add_debug_info('DILocation', {
            'line': line,
            'column': 1,
            'scope': self.subprograms[-1],
        })


class NvvmDIBuilder(DIBuilder):
    """
    Only implemented the minimal metadata to get line number information.
    See http://llvm.org/releases/3.4/docs/LangRef.html
    """
    # These constants are copied from llvm3.4
    DW_LANG_Python = 0x0014
    DI_Compile_unit = 786449
    DI_Subroutine_type = 786453
    DI_Subprogram = 786478
    DI_File = 786473

    DWARF_VERSION = None  # don't emit DWARF version
    DEBUG_INFO_VERSION = 1  # as required by NVVM IR Spec
    # Rename DIComputeUnit MD to hide it from llvm.parse_assembly()
    # which strips invalid/outdated debug metadata
    DBG_CU_NAME = 'numba.llvm.dbg.cu'

    # Default member
    # Used in mark_location to remember last lineno to avoid duplication
    _last_lineno = None

    def mark_variable(self, builder, allocavalue, name, lltype, size, loc):
        # unsupported
        pass

    def mark_location(self, builder, loc):
        # Avoid duplication
        if self._last_lineno == loc.line:
            return
        self._last_lineno = loc.line
        # Add call to an inline asm to mark line location
        asmty = ir.FunctionType(ir.VoidType(), [])
        asm = ir.InlineAsm(asmty, "// dbg {}".format(loc.line), "",
                           side_effect=True)
        call = builder.call(asm, [])
        md = self._di_location(loc.line)
        call.set_metadata('numba.dbg', md)

    def mark_subprogram(self, function, name, loc):
        self._add_subprogram(name=name, linkagename=function.name,
                             line=loc.line)

    #
    # Helper methods to create the metadata nodes.
    #

    def _filepair(self):
        return self.module.add_metadata([
            os.path.basename(self.filepath),
            os.path.dirname(self.filepath),
        ])

    def _di_file(self):
        return self.module.add_metadata([
            self._const_int(self.DI_File),
            self._filepair(),
        ])

    def _di_compile_unit(self):
        filepair = self._filepair()
        empty = self.module.add_metadata([self._const_int(0)])
        return self.module.add_metadata([
            self._const_int(self.DI_Compile_unit),         # tag
            filepair,                   # source directory and file pair
            self._const_int(self.DW_LANG_Python),  # language
            'Numba',                     # producer
            self._const_bool(True),      # optimized
            "",                          # flags??
            self._const_int(0),          # runtime version
            empty,                       # enums types
            empty,                       # retained types
            self.module.add_metadata(self.subprograms),  # subprograms
            empty,                       # global variables
            empty,                       # imported entities
            "",                          # split debug filename
        ])

    def _di_subroutine_type(self):
        types = self.module.add_metadata([None])
        return self.module.add_metadata([
            self._const_int(self.DI_Subroutine_type),                # tag
            self._const_int(0),
            None,
            "",
            self._const_int(0),                 # line of definition
            self._const_int(0, 64),             # size in bits
            self._const_int(0, 64),             # offset in bits
            self._const_int(0, 64),             # align in bits
            self._const_int(0),                 # flags
            None,
            types,
            self._const_int(0),
            None,
            None,
            None,
        ])

    def _di_subprogram(self, name, linkagename, line):
        function_ptr = self.module.get_global(linkagename)
        subroutine_type = self._di_subroutine_type()
        funcvars = self.module.add_metadata([self._const_int(0)])
        context = self._di_file()
        return self.module.add_metadata([
            self._const_int(self.DI_Subprogram),   # tag
            self._filepair(),          # source dir & file
            context,                   # context descriptor
            name,                      # name
            name,                      # display name
            linkagename,               # linkage name
            self._const_int(line),     # line
            subroutine_type,           # type descriptor
            self._const_bool(False),   # is local
            self._const_bool(True),    # is definition
            self._const_int(0),        # virtuality
            self._const_int(0),        # virtual function index
            None,                     # vtable base type
            self._const_int(0),        # flags
            self._const_bool(True),    # is optimized
            function_ptr,              # pointer to function
            None,                      # function template parameters
            None,                      # function declaration descriptor
            funcvars,                     # function variables
            self._const_int(line)      # scope line
        ])

    def _di_location(self, line):
        return self.module.add_metadata([
            self._const_int(line),   # line
            self._const_int(0),      # column
            self.subprograms[-1],    # scope
            None,                    # original scope
        ])

