#!/usr/bin/env python
# I, Danny Milosavljevic, hereby place this file into the public domain.
import ram
import memory
import sys
import rom
import colorram
minimal_overlay_address = 0xA000

class C64Configurator(memory.Memory): # $0..$1
    # TODO maybe handle data_direction properly here (doesn't make all that much sense since we don't know the hardware defaults if there is NOBODY to set the lines).
    def __init__(self, MMU):
        memory.Memory.__init__(self)
        self.B_can_write = True # in the instance because of ShedSkin
        self.B_active = True
        self.MMU = MMU
        self.data_direction = 0x2F # RW for port below
        self.port = 0
        """
        port:
            bits 0..2: configuration for ROM memory:
                bx00: RAM visible in all areas.
                bx01: RAM visible at $A000..BFFF, $E000..$FFFF.
                bx10: RAM visible at $A000..BFFF, KERNAL ROM AT $E000..$FFFF.
                bx11: BASIC ROM visible at $A000..BFFF, KERNAL ROM AT $E000..$FFFF.
                b0xx: Character ROM visible at $D000..$DFFF except for b000.
                b1xx: IO area visible at $D000..$DFFF except for b100.
            bit 3: datasette output signal level
            bit 4: datasette button status (inverse, input).
            bit 5: motor control (inverse).
        """

    def read_memory(self, address, size = 1):
        assert size == 1, "CPUPort.read_memory size=1"
        if address == 0:
            return self.data_direction
        elif address == 1:
            return self.port
        else:
            assert False, "CPUPort.read_memory address is known"
            return 0

    def write_memory(self, address, value, size):
        assert isinstance(value, int), "CPUPort.write_memory value written is an integer"
        #print("CPUPort writing to %r: value %r" % (address, value))
        if address == 0:
            self.data_direction = value
        elif address == 1:
            self.set_port(value)
        else:
            assert False, "CPUPort.write_memory address is known"

    def set_port(self, value):
        if self.port == value:
            return
        #if value & 32:
        #    print("cassette motor off")
        #else:
        #    print("cassette motor on")
        self.port = value
        B_all_visible = (value & 3) == 0 # all RAM
        low = (value & 3)
        B_A000_RAM = low == 1 or low == 0x10 or B_all_visible
        B_E000_RAM = low == 1 or B_all_visible
        B_D000_Character = (value & 4) == 0 and not B_all_visible
        B_D000_IO = (value & 4) != 0 and not B_all_visible
        self.MMU.set_overlay_active("basic", not B_A000_RAM)
        self.MMU.set_overlay_active("chargen", B_D000_Character)
        self.MMU.set_overlay_active("vic", B_D000_IO)
        self.MMU.set_overlay_active("sid", B_D000_IO)
        self.MMU.set_overlay_active("cia1", B_D000_IO)
        self.MMU.set_overlay_active("cia2", B_D000_IO)
        self.MMU.set_overlay_active("kernal", not B_E000_RAM)
        if B_E000_RAM:
            sys.stderr.write("warning: KERNAL disabled!!!\n")
            #time.sleep(5.0)
        self.update_cache()
    def update_cache(self):
        cache_map = self.MMU.update_cache_map(self.port, 0, 0, 1)
        self.cache_map = cache_map
    def dump_state(self):
        result = []
        for i in range(2):
            result.append(self.read_memory(i, 1))
        return(result)
    def restore_state(self, state):
        if len(state) > 0:
            for i in range(2):
                self.write_memory(i, state[i], 1)

"""
C128:
# set memory configuration by writing to $FF00
# BASIC: RAM bank 0 for code, RAM bank 1 for variables.
# also sets VIC bank (independently)
# BASIC text normally begins at $1C01 instead of at $0801. 
# GRAPHIC: start of BASIC text is moved to $4001 (under BASIC ROM). 
# BASIC and machine code monitor ROM ($4000-$BFFF)
# screen editor: $C000-$CFFF
# $1300-$1BFF is free. 
# $FA-$FE are free but $FA is unsafe to use due to a bug in the Kernal.
# superbank size is 64K.
"""
class C128Configurator(memory.Memory): # $D500
    def __init__(self, MMU, B_active = True):
        self.MMU = MMU
        self.B_active = B_active
        self.registers = 17*[0] # FIXME initial values. First is correct.
        self.registers[5] = 0xA6 # FIXME initial value.
        self.registers[7] = 0 # page# zero page.
        self.registers[8] = 0xF0 # upper 4 bits are junk.
        self.registers[9] = 1
        self.registers[0xA] = 0xF0 # upper 4 bits are junk.
        self.registers[0xB] = 0x20 # 2 superbanks of RAM, version 0.
        self.B_can_write = True # in the instance because of ShedSkin
        #self.selected_banks = 64*[0] # 1K each (one exception) # FIXME initial values.
        self.page0_page = 0
        self.page1_page = 0
        #this would break if not all others are already there. cache_map = self.MMU.update_cache_map(self.registers[0], self.registers[6], self.page0_page, self.page1_page)
        #self.pre_cache_maps = 5*[cache_map] # first is dummy
    def read_memory(self, address, size = 1):
        if size == 1:
            return self.registers[address]
        return memory.one_big_value(self.registers[address : address + size])
    def write_memory(self, address, value, size):
        assert(size == 1)
        if address < 17:
            self.registers[address] = value
            if address == 0: # IO mapper
                #superbank = (value >> 6) & 3 # bank#
                #B_D000_E000_IO = (value & 1) == 0
                #B_4000_8000_ROM = (value & 2) == 0
                #M_8000_C000_mode = (value >> 2) & 3 # 0=BasicROMHigh, 01=internROM, 10=externROM, 11=RAM
                #M_C000_FF_mode = (value >> 4) & 3 # 0=ROM, 01=internROM, 10=externROM, 11=RAM
                #if B_4000_8000_ROM:
                #    self.banks0[0] = superbank
                #if M_8000_C000_mode == 3:
                #    self.banks0[1] = superbank
                #if M_C000_FF_mode == 3:
                #    self.banks0[2] = superbank
                self.update_cache()
            elif address < 5:
                # TODO use
                pass # self.pre_cache_maps[address] = self.MMU.calc_cache_map(self.registers[0], self.registers[6], self.page0_page, self.page1_page)
            elif address == 5:
                self.B_activate_C64 = (value & 1) != 0
            elif address == 6: # 64K superbank switcher
                self.update_cache()
            elif address == 7:
                self.page0_page = value | (self.registers[8] << 8)
                self.update_cache()
            elif address == 8:
                pass
            elif address == 9:
                self.page1_page = value | (self.registers[0xA] << 8)
                self.update_cache()
            elif address == 0xA:
                pass
            # TODO A change needs to first hit P0H (or P1H). The value will be cached until P0L (or P1L) was changed as well.
    def update_cache(self):
        cache_map = self.MMU.update_cache_map(self.registers[0], self.registers[6], self.page0_page, self.page1_page)
        self.cache_map = cache_map
    #def update_cache(self):
    #    #self.selected_banks = calc_C128_cache_map(self.registers[0], self.registers[6], self.page0_page, self.page1_page)
    def dump_state(self):
        result = []
        for i in range(len(self.registers)):
            result.append(self.registers[i])
        return(result)
    def restore_state(self, state):
        if len(state) > 0:
            registers = state
            for i in range(len(registers)):
                self.write_memory(i, registers[i], 1)

class C128ShadowConfigurator(memory.Memory): # $FF00
    # don't have state here.
    def __init__(self, configurator, B_active = True):
        self.configurator = configurator
        self.B_active = B_active
        self.B_can_write = True # in the instance because of ShedSkin
    def read_memory(self, address, size = 1):
        return(self.configurator.read_memory(address, size))
    def write_memory(self, address, value, size):
        assert(size == 1)
        if address == 0:
            self.configurator.write_memory(0, value, size)
        elif address >= 1 and address < 5:
            self.configurator.write_memory(0, self.configurator.read_memory(address, 1), 1)
        else:
            print("C128MMUF: ignored write to $%X." % address)

# see <ftp://n2dvm.com/Commodore/Commie-CDs/Kazez%20FREE-CD/c64-knowledge-base/022.htm>

class Window1(memory.Memory):
    def __init__(self, underlying, base_offset):
        memory.Memory.__init__(self)
        self.B_can_write = True
        self.underlying = underlying
        self.base_offset = base_offset
    def write_memory(self, address, value, size):
        self.underlying.write_memory(address + self.base_offset, value, size)
    def read_memory(self, address, size = 1):
        return(self.underlying.read_memory(address + self.base_offset, size))

def hardware_overlay_P(address):
    return(address == 0xC00 or address == 0xC01 or address == 0xD00 or address == 0xD01) # TODO D1541 if applicable.
                                        
class MMU(memory.Memory):
    def __init__(self):
        self.IO_pages = 256*[None]
        self.basic_pages = 256*[None]
        self.BASICLOW_pages = 256*[None]
        self.BASICHIGH_pages = 256*[None]
        self.INTERNALFUNCTIONROM_pages = 256*[None]
        self.EXTERNALFUNCTIONROM_pages = 256*[None]
        self.kernal_pages = 256*[None]
        self.chargen_pages = 256*[None]
        self.known_addresses = {}
        self.lifeline_controller = None
        self.memory = 256*256*[0]
        self.IO_overlay = 0xDE00*[0xFF]
        self.color_pages = 256*[None]
        self.color_pages[0] = memory.Memory() # ShedSkin
    def set_known_addresses(self, value):
        self.known_addresses = value
    def add_real_page(self, prefix, page_number, controller):
        if prefix == "RAM":
            pass
        elif prefix == "basic":
            self.basic_pages[page_number] = controller
        elif prefix == "IO":
            self.IO_pages[page_number] = controller
        elif prefix == "kernal":
            self.kernal_pages[page_number] = controller
        elif prefix == "BASICLOW":
            self.BASICLOW_pages[page_number] = controller
        elif prefix == "BASICHIGH":
            self.BASICHIGH_pages[page_number] = controller
        elif prefix == "cpu":
            if page_number == 0: # C64
                self.CPU_controller = controller
            if page_number == 255: # C128
                self.lifeline_controller = controller
    def read_memory(self, address, size = 1):
        page_number = address >> 8
        offset = address & 0xFF
        if ((address + size - 1) >> 8) > address >> 8: # crossing the page boundary
            assert(size == 2)
            return self.read_memory(address, 1) | (self.read_memory(address + 1, 1) << 8)
        controller = self.CPU_controller if page_number == 0 and address < 2 else self.real_pages[page_number]
        if offset < 5 and page_number == 255 and self.lifeline_controller is not None:
            controller = self.lifeline_controller
        if controller is None:
            if size == 1:
                result = self.memory[address]
            else:
                result = memory.one_big_value(self.memory[address : address + size])
        else:
            result = controller.read_memory(offset, size)
        if address >= 0xD000 and address < 0xE000 and self.get_B_IO(): # IO
            IO_addr = address & 0xFFF
            if hardware_overlay_P(IO_addr):
                #dir_addr = hardware_direction_address_of(IO_addr)|0xD000
                # assume also visible
                #d = self.IO_pages[dir_addr >> 8].read_memory(dir_addr & 0xFF, size) # that is, self.read_memory(dir_addr | 0xD000)
                vo = self.IO_overlay[IO_addr] if size == 1 else memory.one_big_value(self.IO_overlay[IO_addr : IO_addr + size]) # you really don't want the latter.
                result = vo # (result&d) | (vo&~d)
        return(result)
    def write_memory(self, address, value, size):
        page_number = address >> 8
        offset = address & 0xFF 
        if ((address + size - 1) >> 8) > address >> 8: # crossing the page boundary
            assert(size == 2)
            self.write_memory(address, value & 0xFF, 1)
            self.write_memory(address + 1, value >> 8, 1)
            return
        controller = self.CPU_controller if page_number == 0 and address < 2 else self.real_pages[page_number]
        if offset < 5 and page_number == 255 and self.lifeline_controller is not None:
            controller = self.lifeline_controller
        if controller is None or not controller.B_can_write: # can't write to ROM
            if size == 1:
                self.memory[address] = value
            else:
                for i in range(size):
                    self.memory[address + i] = value & 0xFF
                    value >>= 8
        else:
            controller.write_memory(offset, value, size)
    def VIC_read_RAM(self, address):
        """ VIC special case, NOT (necessarily) color ram, but their own view of the world. 
            Technically, I'd have to have a VICMMU but I really can't be bothered (for now) """
        controller = self.color_pages[address>>8]
        if controller is not None:
            return controller.read_memory(address&0xFF, 1)
        else:
            return self.memory[address]
    def read_chargen(self, address, size = 1): # VIC special case
        return self.chargen_ROM.read_memory(address, size)
    def get_B_IO(self):
        return(False)
    def calc_cache_map(self, value, wvalue, page0_page, page1_page):
        result = []
        result.append(self.IO_pages[0]) # ShedSkin dummy
        return(result)
    def update_cache_map(self, value, wvalue, page0_page, page1_page):
        self.real_pages = self.calc_cache_map(value, wvalue, page0_page, page1_page)
        return(self.real_pages)
    def map_ROM(self, name, address, value, B_active): # this doesn't actually MAP anything, but it's to keep compability with the old interface.
        assert address >= minimal_overlay_address, "MMU.map_ROM address >= minimal_overlay_address" # ??  or range_1[1] == 2
        assert((address & 0xFF) == 0)
        r = value
        for page in range(address >> 8, (address + len(value) + 255) >> 8):
            ROM_1 = rom.ROM(r[:256], B_active) # B_active is ignored
            r = r[256:]
            self.add_real_page(name, page, ROM_1)
        if name == "chargen":
            self.chargen_ROM = rom.ROM(value, True)
        return(ROM_1)
    def map_color_IO(self, name, range_1):
        assert range_1[0] >= minimal_overlay_address or range_1[1] == 0x100, "MMU.map_IO"
        assert((range_1[0] & 0xFF) == 0)
        assert((range_1[1] & 0xFF) == 0)
        #RAM_trap = RAMTrap(self.memory, 0xD800)
        #self.map_IO(name, range_1, RAM_trap)
        for page in range(range_1[0] >> 8, range_1[1] >> 8):
            self.color_pages[page] = colorram.ColorRAM()
            self.add_real_page("IO", page, self.color_pages[page]) # on the real C64, those are always switched together with IO, so I assume they are IO.
    def map_IO(self, name, range_1, IO):
        assert range_1[0] >= minimal_overlay_address or range_1[1] == 0x100, "MMU.map_IO"
        assert((range_1[0] & 0xFF) == 0)
        assert((range_1[1] & 0xFF) == 0)
        if (range_1[0] >> 8 < (range_1[1] - 1) >> 8): # more than one page total
            off = 0
            for page in range(range_1[0] >> 8, range_1[1] >> 8):
                self.add_real_page("IO" if name != "cpu" else name, page, Window1(IO, off))
                off += 256
        else:
            assert(range_1[0] >> 8 == (range_1[1] - 1) >> 8)
            for page in range(range_1[0] >> 8, range_1[1] >> 8):
                self.add_real_page("IO" if name != "cpu" else name, page, IO)
    def set_overlay_active(self, name, value):
        pass # unused
    def IO_overlay_write_byte(self, IO_address, value):
        IO_address = IO_address & 0xFFF
        self.IO_overlay[IO_address] = value
    def dump_state(self):
        result = self.IO_overlay
        # note that actual configuration is handled by configurator
        return(result)
    def restore_state(self, state):
        if len(state) > 0:
            IO_overlay = state
            for i in range(len(self.IO_overlay)):
                self.IO_overlay[i] = IO_overlay[i]
        # note that actual configuration is handled by configurator

class C128MMU(MMU):
    def get_B_IO(self):
        value = 0 # FIXME
        B_D000_E000_IO = (value & 1) == 0 
        return(B_D000_E000_IO)
    def calc_cache_map(self, value, wvalue, page0_page, page1_page):
        """ returns a map of page -> frame where one page is 256 bytes """
        # $0000..$0001 and $FF00..$FF04 are ALWAYS visible everywhere (and the same). These are special-cased elsewhere.
        #value = registers[0]
        selected_pages = 256*[None] # bullshit, will be fixed up
        superbank = (value >> 6) & 3 # bank#
        actual_page_base = superbank * 0x100
        B_D000_E000_IO = (value & 1) == 0
        B_4000_8000_ROM = (value & 2) == 0
        M_8000_C000_mode = (value >> 2) & 3 # 0=BasicROMHigh, 01=internROM, 10=externROM, 11=RAM
        M_C000_FF_mode = (value >> 4) & 3 # 0=ROM, 01=internROM, 10=externROM, 11=RAM
        if True: # non-IO
                if B_4000_8000_ROM:
                    for page_offset in range(0x4000//256, 0x8000//256):
                        selected_pages[page_offset] = None # not subject to window
                if M_8000_C000_mode == 3: # FIXME char rom etc
                    for page_offset in range(0x8000//256, 0xC000//256):
                        selected_pages[page_offset] = None 
                if M_C000_FF_mode == 3: # FIXME char rom etc
                    for page in range(0xC000//256, 0x10000//256):
                        selected_pages[page_offset] = None
        if True: # windows
                VIC_superbank = (wvalue & 64) != 0
                B_top_window = (wvalue & 8) != 0 # stretch from either $C000, $E000, $F000, or $FC00 
                B_bottom_window = (wvalue & 4) != 0 # stretches from $0002 to $03FF, $0FFF $1FFF, or $3FFF
                window_size_mode = (wvalue & 3)  # 00=1K, 01=4K, 10=8K, 11=16K
                window_size = 2**5*2**window_size_mode if window_size_mode > 0 else 1 # in pages
                if B_top_window:
                    for page_offset in range(0x100 - window_size, 0x100):
                        selected_pages[page_offset] = None
                if B_bottom_window: # excludes 0,1
                    for page_offset in range(0, window_size):
                        selected_pages[page_offset] = None
        if True: # ROM
                #M_selected_mode = banks0[0]
                if B_4000_8000_ROM:
                    for page_offset in range(0x4000//256, 0x8000//256):
                        selected_pages[page_offset] = self.BASICLOW_pages[actual_page_base + page_offset]
                #M_selected_mode = banks0[1]
                if M_8000_C000_mode != 3:
                    for page_offset in range(0x8000//256, 0xC000//256):
                        if M_8000_C000_mode == 1:
                            selected_pages[page_offset] = self.INTERNALFUNCTIONROM_pages[actual_page_base + page_offset]
                        elif M_8000_C000_mode == 2:
                            selected_pages[page_offset] = self.EXTERNALFUNCTIONROM_pages[actual_page_base + page_offset]
                        else:
                            selected_pages[page_offset] = self.BASICHIGH_pages[actual_page_base + page_offset]
                #M_selected_mode = banks0[2]
                if M_C000_FF_mode != 3:
                    if M_C000_FF_mode == 1:
                        for page_offset in range(0xC000//256, 0x10000//256):
                            selected_pages[page_offset] = self.INTERNALFUNCTIONROM_pages[actual_page_base + page_offset]
                    elif M_C000_FF_mode == 2:
                        for page_offset in range(0xC000//256, 0x10000//256):
                            selected_pages[page_offset] = self.EXTERNALFUNCTIONROM_pages[actual_page_base + page_offset]
                    else:
                        for page_offset in range(0xC000//256, 0x10000//256):
                            selected_pages[page_offset] = self.kernal_pages[actual_page_base + page_offset]
        # now shrink it to 1K pages in order to get rid of the special case for IO
        result = selected_pages
        if B_D000_E000_IO: # not the entire page, actually, so special-case it.
            for page_offset in range(0xD000//256, 0xE000//256):
                result[page_offset] = self.IO_pages[page_offset]
        a = result[page0_page]
        result[1] = result[page1_page]
        result[0] = a
        return(result)

class C64MMU(MMU):
    def get_B_IO(self):
        value = self.CPU_controller.read_memory(0)
        B_all_visible = (value & 3) == 0 # all RAM
        B_D000_IO = (value & 4) != 0 and not B_all_visible
        return(B_D000_IO)
    def calc_cache_map(self, value, wvalue, page0_page, page1_page):
        result = []
        for page_offset in range(0x0000//256, 0x10000//256):
           result.append(None)
        B_all_visible = (value & 3) == 0 # all RAM
        low = (value & 3)
        B_A000_RAM = low == 1 or low == 0x10 or B_all_visible
        B_E000_RAM = low == 1 or B_all_visible
        B_D000_Character = (value & 4) == 0 and not B_all_visible
        B_D000_IO = (value & 4) != 0 and not B_all_visible
        if not B_A000_RAM:
            for page_offset in range(0xA000//256, 0xC000//256):
                result[page_offset] = self.basic_pages[page_offset]
        if B_D000_Character: # FIXME priority
            for page_offset in range(0xD000//256, 0xE000//256):
                result[page_offset] = self.chargen_pages[page_offset]
        elif B_D000_IO: # VIC, SID, CIA1, CIA2, KERNAL
            for page_offset in range(0xD000//256, 0xE000//256):
                result[page_offset] = self.IO_pages[page_offset]
        if not B_E000_RAM:
            for page_offset in range(0xE000//256, 0x10000//256):
                result[page_offset] = self.kernal_pages[page_offset]
        return(result)

class D1541MMU(MMU):
    def get_B_IO(self):
        # value = self.CPU_controller.read_memory(0)
        B_IO = True # TODO can that be unmapped?
        return(B_IO)
    def calc_cache_map(self, value, wvalue, page0_page, page1_page):
        """ returns a map of page -> frame where one page is 256 bytes """
        selected_pages = 256*[None] # bullshit, will be fixed up # probably too large.
        B_IO = True # TODO can that be unmapped?
        B_ROM = True # TODO can that be unmapped?
        result = selected_pages
        if B_ROM:
            for page_offset in range(0x8000//256, 0x10000//256):
                result[page_offset] = self.kernal_pages[page_offset]
        if B_IO:
            for page_offset in range(0x1800//256, 0x2000//256):
                result[page_offset] = self.IO_pages[page_offset]
        return(result)
