# Copyright 2017 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


from typing import List, Tuple
from dataclasses import dataclass

import viv_utils
import envi.memory
import vivisect.const
import viv_utils.emulator_drivers
from envi import Emulator

import floss.utils
import floss.logging_

from . import api_hooks
from .const import DS_MAX_ADDRESS_REVISITS_EMULATION

logger = floss.logging_.getLogger(__name__)
MAX_MAPS_SIZE = 1024 * 1024 * 100  # 100MB max memory allocated in an emulator instance


def is_import(emu, va):
    """
    Return True if the given VA is that of an imported function.
    """
    t = emu.getVivTaint(va)
    if t is None:
        return False

    _, ttype, tinfo = t
    return ttype == "import" and isinstance(tinfo, tuple) and len(tinfo) >= 3 and tinfo[2] == vivisect.const.LOC_IMPORT


# type aliases for envi.memory map
MemoryMapDescriptor = Tuple[
    # va
    int,
    # size
    int,
    # perms
    int,
    # name
    str,
]

# type aliases for envi.memory map
MemoryMap = Tuple[
    # start
    int,
    # end
    int,
    # descriptor
    MemoryMapDescriptor,
    # content
    bytes,
]

# type aliases for envi.memory map
Memory = List[MemoryMap]


@dataclass
class Snapshot:
    """
    A snapshot of the state of the CPU and memory.

    Attributes:
        memory: a snapshot of the memory contents
        sp: the stack counter
        pc: the instruction pointer
    """

    memory: Memory
    sp: int
    pc: int


def get_map_size(emu):
    size = 0
    for mapva, mapsize, mperm, mfname in emu.getMemoryMaps():
        mapsize += size
    return mapsize


class MapsTooLargeError(Exception):
    pass


def make_snapshot(emu: Emulator) -> Snapshot:
    """
    Create a snapshot of the current CPU and memory.
    """
    if get_map_size(emu) > MAX_MAPS_SIZE:
        logger.debug("emulator mapped too much memory: 0x%x", get_map_size(emu))
        raise MapsTooLargeError()
    return Snapshot(emu.getMemorySnap(), emu.getStackCounter(), emu.getProgramCounter())


@dataclass
class Delta:
    """
    a pair of snapshots from before and after an operation.
    facilitates diffing the state of an emulator.
    """

    pre: Snapshot
    post: Snapshot


class DeltaCollectorHook(viv_utils.emulator_drivers.Hook):
    """
    hook that collects Deltas at each imported API call.
    """

    def __init__(self, pre_snap: Snapshot):
        super().__init__()
        self._pre_snap = pre_snap
        self.deltas: List[Delta] = []

    def __call__(self, emu, api, argv):
        if is_import(emu, emu.getProgramCounter()):
            # TODO add apis to ignore here, e.g.
            #  "kernel32.GetSystemTime", "ntdll.RtlFreeHeap", "ntdll.RtlAllocateHeap",
            #  callname = driver._emu.getCallApi(driver._emu.getProgramCounter())[3]
            try:
                # TODO optimize - may leverage writelog
                #  reduce duplicate deltas
                #  reduce redundant (unchanged) data in each delta
                self.deltas.append(Delta(self._pre_snap, make_snapshot(emu)))
            except MapsTooLargeError:
                _, _, _, name, _ = api
                logger.debug("despite call to import %s, maps too large, not extracting strings", name)
                pass


def emulate_function(
    emu: Emulator, function_index, fva: int, return_address: int, max_instruction_count: int
) -> List[Delta]:
    """
    Emulate a function and collect snapshots at each interesting place.
    These interesting places include calls to imported API functions
     and the final state of the emulator.
    Emulation continues until the return address is hit, or
     the given max_instruction_count is hit.
    Some library functions are shimmed, such as memory allocation routines.
    This helps "normal" routines emulate correct using standard library function.
    These include:
      - GetProcessHeap
      - RtlAllocateHeap
      - AllocateHeap
      - malloc

    :type function_index: viv_utils.FunctionIndex
    :param fva: The start address of the function to emulate.
    :param return_address: The expected return address of the function.
     Emulation stops here.
    :param max_instruction_count: The max number of instructions to emulate.
     This helps avoid unexpected infinite loops.
    """
    try:
        pre_snap = make_snapshot(emu)
    except MapsTooLargeError:
        logger.warning("initial snapshot mapped too much memory, can't extract strings")
        return []

    delta_collector = DeltaCollectorHook(pre_snap)

    try:
        logger.debug("Emulating function at 0x%08x", fva)
        driver = viv_utils.emulator_drivers.DebuggerEmulatorDriver(
            emu, repmax=256, max_hit=DS_MAX_ADDRESS_REVISITS_EMULATION, max_insn=max_instruction_count
        )
        monitor = api_hooks.ApiMonitor(function_index)
        driver.add_monitor(monitor)
        driver.add_hook(delta_collector)

        with api_hooks.defaultHooks(driver):
            driver.run_to_va(return_address)

    except viv_utils.emulator_drivers.BreakpointHit as e:
        # TODO track/shortcut instances of this
        if e.reason == "max_insn":
            logger.debug("Halting as emulation has escaped!")
    except envi.InvalidInstruction as e:
        logger.debug("vivisect encountered an invalid instruction. will continue processing. %s", e)
    except envi.UnsupportedInstruction as e:
        logger.debug("vivisect encountered an unsupported instruction. will continue processing. %s", e)
    except envi.BreakpointHit as e:
        logger.debug("vivisect encountered an unexpected emulation breakpoint. will continue processing. %s", e)
    except envi.exc.SegmentationViolation as e:
        tos_val = floss.utils.get_stack_value(emu, 0)
        logger.debug("%s: top of stack (return address): 0x%x", e, tos_val)
    except envi.exc.DivideByZero as e:
        logger.debug("vivisect encountered an emulation error. will continue processing. %s", e)
    except viv_utils.emulator_drivers.StopEmulation:
        pass
    except Exception:
        # we cheat here a bit and skip over various errors, check this for improvements and debugging
        logger.debug("vivisect encountered an unexpected exception. will continue processing.", exc_info=True)
    logger.debug("Ended emulation at 0x%08x", emu.getProgramCounter())

    deltas = delta_collector.deltas

    try:
        deltas.append(Delta(pre_snap, make_snapshot(emu)))
    except MapsTooLargeError:
        logger.debug("failed to create final snapshot, emulator mapped too much memory, skipping")
        pass

    return deltas
