"""
RiscEmu (c) 2021 Anton Lydike

SPDX-License-Identifier: MIT
"""

from typing import Dict, List, Optional

from .colors import *
from .exceptions import InvalidAllocationException, MemoryAccessException
from .helpers import align_addr, int_from_bytes
from .types import Instruction, MemorySection, MemoryFlags, T_AbsoluteAddress, \
    Program, InstructionContext


class MMU:
    """
    The MemoryManagementUnit. This provides a unified interface for reading/writing data from/to memory.

    It also provides various translations for addresses.
    """

    max_size = 0xFFFFFFFF
    """
    The maximum size of the memory in bytes
    """

    max_alloc_size = 8 * 1024 * 1024 * 64
    """
    No single allocation can be bigger than 64 MB
    """

    sections: List[MemorySection]
    """
    A list of all loaded memory sections
    """

    programs: List[Program]
    """
    A list of all loaded programs
    """

    global_symbols: Dict[str, int]
    """
    The global symbol table
    """

    def __init__(self):
        """
        Create a new MMU
        """
        self.programs = list()
        self.sections = list()
        self.global_symbols = dict()

    def get_sec_containing(self, addr: T_AbsoluteAddress) -> Optional[MemorySection]:
        """
        Returns the section that contains the address addr

        :param addr: the Address to look for
        :return: The LoadedMemorySection or None
        """
        for sec in self.sections:
            if sec.base <= addr < sec.base + sec.size:
                return sec
        return None

    def get_bin_containing(self, addr: T_AbsoluteAddress) -> Optional[Program]:
        for program in self.programs:
            if program.base <= addr < program.base + program.size:
                return program
        return None

    def read_ins(self, addr: T_AbsoluteAddress) -> Instruction:
        """
        Read a single instruction located at addr

        :param addr: The location
        :return: The Instruction
        """
        sec = self.get_sec_containing(addr)
        if sec is None:
            print(FMT_MEM + "[MMU] Trying to read instruction form invalid region! (read at {}) ".format(addr)
                  + "Have you forgotten an exit syscall or ret statement?" + FMT_NONE)
            raise RuntimeError("No next instruction available!")
        return sec.read_ins(addr - sec.base)

    def read(self, addr: int, size: int) -> bytearray:
        """
        Read size bytes of memory at addr

        :param addr: The addres at which to start reading
        :param size: The number of bytes to read
        :return: The bytearray at addr
        """
        sec = self.get_sec_containing(addr)
        if sec is None:
            print(FMT_MEM + "[MMU] Trying to read data form invalid region at 0x{:x}! ".format(addr) + FMT_NONE)
            raise MemoryAccessException("region is non-initialized!", addr, size, 'read')
        return sec.read(addr - sec.base, size)

    def write(self, addr: int, size: int, data):
        """
        Write bytes into memory

        :param addr: The address at which to write
        :param size: The number of bytes to write
        :param data: The bytearray to write (only first size bytes are written)
        """
        sec = self.get_sec_containing(addr)
        if sec is None:
            print(FMT_MEM + '[MMU] Invalid write into non-initialized region at 0x{:08X}'.format(addr) + FMT_NONE)
            raise MemoryAccessException("region is non-initialized!", addr, size, 'write')

        return sec.write(addr - sec.base, size, data)

    def dump(self, addr, *args, **kwargs):
        """
        Dumpy the memory contents

        :param addr: The address at which to dump
        :param args: args for the dump function of the loaded memory section
        :param kwargs: kwargs for the dump function of the loaded memory section
        """
        sec = self.get_sec_containing(addr)
        if sec is None:
            print(FMT_MEM + "[MMU] No section containing addr 0x{:08X}".format(addr) + FMT_NONE)
            return
        sec.dump(addr, *args, **kwargs)

    def label(self, symb: str):
        """
        Look up the symbol symb in all local symbol tables (and the global one)

        :param symb: The symbol name to look up
        """
        print(FMT_MEM + "[MMU] Lookup for symbol {}:".format(symb) + FMT_NONE)
        if symb in self.global_symbols:
            print("   Found global symbol {}: 0x{:X}".format(symb, self.global_symbols[symb]))
        for section in self.sections:
            if symb in section.context.labels:
                print("   Found local labels {}: 0x{:X} in {}".format(symb, section.context.labels[symb], section.name))

    def read_int(self, addr: int) -> int:
        return int_from_bytes(self.read(addr, 4))

    def translate_address(self, address: T_AbsoluteAddress) -> str:
        # FIXME: proper implementation using the debug info
        return str(address)

    def has_continous_free_region(self, start: int, end: int) -> bool:
        # if we have no sections we are all good
        if len(self.sections) == 0:
            return True
        # if the last section is located before the start we are also good
        if start > self.sections[-1].base + self.sections[-1].size:
            return True

        for sec in self.sections:
            # skip all sections that end before the required start point
            if sec.base + sec.size < start:
                continue
            # we now have the first section that doesn't end **before** the start point
            # if this section starts after the specified end, we are good
            if sec.base > end:
                return True
            # otherwise we can't continue
            return False
        # if all sections end before the requested start we are good
        # technically we shouldn't ever reach this point, but better safe than sorry
        return True

    def load_program(self, program: Program, align_to: int = 4):
        if program.base is not None:
            if not self.has_continous_free_region(program.base, program.base + program.size):
                print(FMT_MEM + "Cannot load program {} into desired space (0x{:0x}-0x{:0x}), area occupied.".format(
                    program.name, program.base, program.base + program.size
                ) + FMT_NONE)
                raise InvalidAllocationException("Area occupied".format(
                    program.name, program.base, program.base + program.size
                ), program.name, program.size, MemoryFlags(False, True))

            at_addr = program.base
        else:
            at_addr = align_addr(self.get_guaranteed_free_address(), align_to)

        # trigger the load event to set all addresses in the binary
        program.loaded_trigger(at_addr)

        # add program and sections to internal state
        self.programs.append(program)
        self.sections += program.sections
        self._update_state()

        # load all global symbols from program
        self.global_symbols.update(
            {key: program.context.labels[key] for key in program.global_labels}
        )
        # inject reference to global symbol table into program context
        # FIXME: this is pretty unclean and should probably be solved in a better way in the future
        program.context.global_symbol_dict = self.global_symbols

    def load_section(self, sec: MemorySection, fixed_position: bool = False) -> bool:
        if fixed_position:
            if self.has_continous_free_region(sec.base, sec.base + sec.size):
                self.sections.append(sec)
                self._update_state()
            else:
                print(FMT_MEM + '[MMU] Cannot place section {} at {}, space is occupied!'.format(sec, sec.base))
                return False
        else:
            at_addr = align_addr(self.get_guaranteed_free_address(), 8)
            sec.base = at_addr
            self.sections.append(sec)
            self._update_state()
            return True

    def _update_state(self):
        """
        Called whenever a section or program is added to keep the list of programs and sections consistent
        :return:
        """
        self.programs.sort(key=lambda bin: bin.base)
        self.sections.sort(key=lambda sec: sec.base)

    def get_guaranteed_free_address(self) -> T_AbsoluteAddress:
        if len(self.sections) == 0:
            return 0x100
        else:
            return self.sections[-1].base + self.sections[-1].size

    def __repr__(self):
        return "MMU(\n\t{}\n)".format(
            "\n\t".join(repr(x) for x in self.programs)
        )

    def context_for(self, addr: T_AbsoluteAddress) -> InstructionContext:
        sec = self.get_sec_containing(addr)

        if sec is not None:
            return sec.context

        return InstructionContext()
