from collections import defaultdict

from py65 import memory


class WriteProtectedException(Exception):
    def __init__(self, name, addr, value):
        self.name = name
        self.addr = addr
        self.value = value

    def __str__(self):
        return 'Write denied to %s ($%04X): $%02X' % (
            self.name, self.addr, self.value)


class UndefinedEntryPointException(Exception):
    def __init__(self, region, prev_addr, addr):
        self.region = region
        self.prev_addr = prev_addr
        self.addr = addr

    def __str__(self):
        return 'Entered region %s via undefined entry point: $%04X --> $%04X' % (
            self.region.name, self.prev_addr, self.addr)


class MemoryRegion:
    def __init__(
            self, name, start, end, read_interceptor=None,
            write_interceptor=None, entrypoints=None,
            writable=True):
        self.name = name
        self.start = start
        self.end = end

        self.read_interceptor = read_interceptor
        self.write_interceptor = write_interceptor

        self.writable = writable

        # Maps PC to handler
        self.entrypoints = entrypoints or {}


class MemoryManager:
    def __init__(self, memory_map):
        self.entrypoints = defaultdict(list)

        default_region = MemoryRegion('default', 0x0, 0xffff)

        def _default_region():
            return default_region

        self.regions = defaultdict(_default_region)

        self.memory = memory.ObservableMemory()

        self._memory_map = memory_map

    def enable(self):
        for region in self._memory_map:
            self.RegisterRegion(region)

    def MaybeInterceptExecution(self, cpu, old_pc):
        pc = cpu.pc
        if pc in self.entrypoints:
            handlers = self.entrypoints[pc]
        else:
            handlers = []

        if self.regions[old_pc] != self.regions[pc]:
            print("Entering region %s" % self.regions[pc].name)
            # Don't worry if last instruction was RTS
            if self.memory[old_pc] != 0x60 and not handlers:
                raise UndefinedEntryPointException(self.regions[pc], old_pc, pc)

        for handler in handlers:
            handler(cpu)

    def RegisterRegion(self, region):
        addr_range = range(region.start, region.end + 1)

        if region.read_interceptor:
            self.memory.subscribe_to_read(addr_range, region.read_interceptor)

        if region.write_interceptor:
            self.memory.subscribe_to_write(addr_range, region.write_interceptor)

        if not region.writable:
            self.memory.subscribe_to_write(
                addr_range, self.DenyWritesToRegion(region))

        for addr in addr_range:
            self.regions[addr] = region

        for addr, handler in region.entrypoints.items():
            self.entrypoints[addr].append(handler)

        # TODO: should trap by default

    @staticmethod
    def DenyWritesToRegion(region):
        def _DenyWritesInterceptor(addr, value):
            raise WriteProtectedException(region.name, addr, value)

        return _DenyWritesInterceptor
