#!/usr/bin/env python3
#
# Cross Platform and Multi Architecture Advanced Binary Emulation Framework
#

"""
This module is intended for general purpose functions that are only used in qiling.arch
"""

from typing import Tuple
from os.path import basename
from functools import lru_cache

from keystone import (Ks, KS_ARCH_ARM, KS_ARCH_ARM64, KS_ARCH_MIPS, KS_ARCH_X86, KS_ARCH_PPC,
    KS_MODE_ARM, KS_MODE_THUMB, KS_MODE_MIPS32, KS_MODE_PPC32, KS_MODE_16, KS_MODE_32, KS_MODE_64,
    KS_MODE_LITTLE_ENDIAN, KS_MODE_BIG_ENDIAN)

from qiling import Qiling
from qiling.const import QL_ARCH, QL_ENDIAN, QL_VERBOSE


class QlArchUtils:
    def __init__(self, ql: Qiling):
        self.ql = ql

        self._disasm_hook = None
        self._block_hook = None

    @lru_cache(maxsize=64)
    def get_base_and_name(self, addr: int) -> Tuple[int, str]:
        for begin, end, _, name, _ in self.ql.mem.map_info:
            if begin <= addr < end:
                return begin, basename(name)

        return addr, '-'

    def disassembler(self, ql: Qiling, address: int, size: int):
        data = ql.mem.read(address, size)

        # knowing that all binary sections are aligned to page boundary allows
        # us to 'cheat' and search for the containing image using the aligned
        # address instead of the actual one.
        #
        # also, the locality property determines that consequent instructions
        # are most likely to reside at the same page in memory, so the containing
        # page of the current instruction is probably the same as the previous
        # one.
        #
        # both assumptions make it possible to cache the search results and pull
        # them off by lru, which provides about 20% speed-up in this case
        ba, name = self.get_base_and_name(ql.mem.align(address))

        anibbles = ql.arch.bits // 4

        for insn in ql.arch.disassembler.disasm(data, address):
            offset = insn.address - ba

            ql.log.info(f'{insn.address:0{anibbles}x} [{name:20s} + {offset:#08x}]  {insn.bytes.hex(" "):20s} {insn.mnemonic:20s} {insn.op_str}')

        if ql.verbose >= QL_VERBOSE.DUMP:
            for reg in ql.arch.regs.register_mapping:
                ql.log.info(f'{reg:10s} : {ql.arch.regs.read(reg):#x}')

    def setup_output(self, verbosity: QL_VERBOSE):
        def ql_hook_block_disasm(ql: Qiling, address: int, size: int):
            self.ql.log.info(f'\nTracing basic block at {address:#x}')

        if self._disasm_hook:
            self._disasm_hook.remove()
            self._disasm_hook = None

        if self._block_hook:
            self._block_hook.remove()
            self._block_hook = None

        if verbosity >= QL_VERBOSE.DISASM:
            try:  # monkey patch disassembler
                from qiling.extensions.r2 import R2
                r2 = R2(self.ql)
                self._disasm_hook = self.ql.hook_code(r2.disassembler)
            except (ImportError, ModuleNotFoundError):
                self._disasm_hook = self.ql.hook_code(self.disassembler)

            if verbosity >= QL_VERBOSE.DUMP:
                self._block_hook = self.ql.hook_block(ql_hook_block_disasm)


# used by qltool prior to ql instantiation. to get an assembler object
# after ql instantiation, use the appropriate ql.arch method
def assembler(arch: QL_ARCH, endianess: QL_ENDIAN, is_thumb: bool) -> Ks:
    """Instantiate an assembler object for a specified architecture.

    Args:
        arch: architecture type
        endianess: architecture endianess
        is_thumb: thumb mode for ARM (ignored otherwise)

    Returns: an assembler object
    """

    endian = {
        QL_ENDIAN.EL: KS_MODE_LITTLE_ENDIAN,
        QL_ENDIAN.EB: KS_MODE_BIG_ENDIAN
    }[endianess]

    thumb = KS_MODE_THUMB if is_thumb else 0

    asm_map = {
        QL_ARCH.CORTEX_M: (KS_ARCH_ARM, KS_MODE_ARM + KS_MODE_LITTLE_ENDIAN + KS_MODE_THUMB),
        QL_ARCH.ARM:      (KS_ARCH_ARM, KS_MODE_ARM + endian + thumb),
        QL_ARCH.ARM64:    (KS_ARCH_ARM64, KS_MODE_ARM),
        QL_ARCH.MIPS:     (KS_ARCH_MIPS, KS_MODE_MIPS32 + endian),
        QL_ARCH.A8086:    (KS_ARCH_X86, KS_MODE_16),
        QL_ARCH.X86:      (KS_ARCH_X86, KS_MODE_32),
        QL_ARCH.X8664:    (KS_ARCH_X86, KS_MODE_64),
        QL_ARCH.PPC:      (KS_ARCH_PPC, KS_MODE_PPC32 + KS_MODE_BIG_ENDIAN)
    }

    if arch in asm_map:
        return Ks(*asm_map[arch])

    raise NotImplementedError
