/*
 * Copyright (c) 2021-2022 Rupert Carmichael
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

// Sega Mega Drive (Genesis)

#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "cega.h"
#include "cega_mixer.h"
#include "cega_m68k.h"
#include "cega_z80.h"
#include "cega_md.h"
#include "cega_md_io.h"
#include "mdvdp.h"
#include "sn76496.h"
#include "ymfm_opn.h"
#include "ymfm_shim.h"

#define MCYC_PER_FRAME_NTSC 896040
#define MCYC_PER_FRAME_PAL 1067040

#define DIV_M68K 7
#define DIV_Z80 15
#define DIV_PSG 16
#define DIV_YM2612 144
#define MCYC_PSG DIV_Z80 * DIV_PSG
#define MCYC_YM2612 DIV_M68K * DIV_YM2612

#define Z80ID 0
#define ZDELAY 3 // Delay Z80 this many cycles when accessing 68K Area

typedef struct _md_header_t {
    const char systype[17];
    const char copyright[17];
    const char title[2][49];
    const char serial[15];
    uint16_t checksum;
    uint8_t devices[16];
    uint8_t range_rom[8];
    uint8_t range_ram[8];
    uint8_t extram[12];
    const char modem[13];
    char region[4];
} md_header_t;

static uint8_t *romdata = NULL; // Game ROM
static size_t romsize = 0; // Size of the ROM in bytes
static md_header_t romheader;

static md_sys_t mdsys;
static sn76496_t psg; // Sega PSG Context

static size_t mcpf = MCYC_PER_FRAME_NTSC;
static uint32_t sramstart = 0;
static uint32_t sramend = 0;

static unsigned mcycs = 0;
static unsigned zcycs = 0;
static unsigned icycs = 0;
static unsigned vcycs = 0;
static unsigned vdpsynced = 0;
static unsigned frameready = 0;

// Sample counters
static unsigned psgcycs = 0;
static unsigned ymcycs = 0;
static unsigned ymsamps = 0;

static unsigned vstored = 0;

// Byte Swapping
static inline uint16_t swapb16(uint16_t x) {
    return (x << 8) | (x >> 8);
}

/* Mega Drive Z80 Memory Map
===========================================
| 0x0000 - 0x1fff | Sound RAM             |
| 0x2000 - 0x3fff | SEGA Reserved         |
| 0x4000 - 0x5fff | YM2612                |
| 0x6000          | Bank Register         |
| 0x6001 - 0x7f10 | Access Prohibited     |
| 0x7f11          | PSG                   |
| 0x7f12 - 0xffff | Access Prohibited     |
-------------------------------------------
*/

// Z80 Reads to 68K Address Space
static uint8_t cega_md_z80_rd_68k(uint16_t addr) {
    return cega_md_m68k_rd08((addr & 0x7fff) | (mdsys.bankreg << 15));
}

// Z80 Writes to 68K Address Space
static void cega_md_z80_wr_68k(uint16_t addr, uint8_t data) {
    cega_md_m68k_wr08((addr & 0x7fff) | (mdsys.bankreg << 15), data);
}

// There are no IO Port mappings for the Z80 on the Mega Drive
static uint8_t cega_md_z80_io_rd(uint8_t port) {
    if (port) { }
    return 0xff;
}

static void cega_md_z80_io_wr(uint8_t port, uint8_t data) {
    if (port || data) { }
}

static uint8_t cega_md_z80_mem_rd(uint16_t addr) {
    if (addr < 0x4000) { // Z80 RAM
        return mdsys.zram[addr & 0x1fff];
    }
    else if (addr < 0x6000) { // All the way to 0x6000 could be wrong FIXME
        /* The system needs to know the YM2612 is working or it will not
           even bother executing the sound management code, which is also
           responsible for controlling the PSG.
        */
        return ym2612_read(addr & 0x03);
    }
    else if (addr < 0x6100) {
        return 0xff;
    }
    else if (addr > 0x6fff && addr < 0x7f21) { // VDP Reads
        cega_md_vdpsync();
        cega_z80_delay(Z80ID, ZDELAY);
        return mdvdp_rd08(addr);
    }
    else if (addr > 0x7fff) { // Read from 68K Address Space
        cega_z80_delay(Z80ID, ZDELAY);
        return cega_md_z80_rd_68k(addr);
    }

    cega_log(CEGA_LOG_WRN, "z80_mem_rd: %04x\n", addr);
    return 0xff;
}

static void cega_md_z80_mem_wr(uint16_t addr, uint8_t data) {
    if (addr < 0x4000) {
        mdsys.zram[addr & 0x1fff] = data;
    }
    else if (addr < 0x6000) { // YM2612 - 4 addresses mirrored
        ym2612_write(addr & 0x03, data);
    }
    else if (addr < 0x6100) {
        /* Z80 Bank Register
           The Z80 Bank Register is a 9-bit shift register that contains the 9
           most significant bits in the 68K's address space which the Z80 will
           read from. The 15 least significant bits are the address within the
           32K bank selected by the 9 bits in the Bank Register. Shift right
           once and assign bit 0 of the data being written to the Bank
           Register's most significant bit.
        */
        mdsys.bankreg = ((mdsys.bankreg >> 1) | (data & 0x01) << 8) & 0x01ff;
    }
    else if (addr == 0x7f11) {
        sn76496_wr(&psg, data);
    }
    else if (addr > 0x6fff && addr < 0x7f21) { // VDP Writes
        cega_md_vdpsync();
        cega_z80_delay(Z80ID, ZDELAY);
        cega_log(CEGA_LOG_WRN, "Z80 VDP Write\n");
    }
    else if (addr > 0x7fff) { // 68K Area
        cega_z80_delay(Z80ID, ZDELAY);
        cega_md_z80_wr_68k(addr, data);
    }
    else
        cega_log(CEGA_LOG_WRN, "z80_mem_wr: %04x %02x\n", addr, data);
}

/* Mega Drive 68K Memory Map
===========================================
| 0x000000 - 0x3fffff | Cartridge ROM     |
| 0x400000 - 0x7fffff | SEGA Reserved     |
| 0x800000 - 0x9fffff | SEGA Reserved     |
| 0xa00000 - 0xa0ffff | Z80 Area          |
| 0xa10000 - 0xa10fff | I/O               |
| 0xa11000 - 0xa11fff | Z80 Control       |
| 0xa12000 - 0xafffff | SEGA Reserved     |
| 0xb00000 - 0xbfffff | SEGA Reserved     |
| 0xc00000 - 0xdfffff | VDP               |
| 0xe00000 - 0xfeffff | Access Prohibited |
| 0xff0000 - 0xffffff | Work RAM          |
-------------------------------------------
*/

static uint8_t cega_md_ioctrl_rd08(uint32_t addr) {
    addr &= 0xffff;

    if (addr < 0x0002) {
        return mdsys.version;
    }
    else if (addr < 0x0020) {
        return cega_md_io_rd(addr);
    }
    else if (addr == 0x1100) {
        uint8_t ret = 0xc0;
        return cega_z80_busack(Z80ID) ? ret : (ret | 0x01);
    }
    else if (addr == 0x1101) {
        return rand() & 0xff;
    }
    else if (addr == 0x1200) {
        uint8_t ret = 0xc0;
        return cega_z80_reset_stat(Z80ID) ? ret : (ret | 0x01);
    }
    else if (addr == 0x1201) {
        return rand() & 0xff;
    }
    else {
        cega_log(CEGA_LOG_WRN, "ioctrl_rd08: %04x\n", addr);
    }

    return 0xff;
}

static uint16_t cega_md_ioctrl_rd16(uint32_t addr) {
    return (cega_md_ioctrl_rd08(addr) << 8) | cega_md_ioctrl_rd08(addr + 1);
}

static void cega_md_ioctrl_wr08(uint32_t addr, uint8_t data) {
    addr &= 0xffff;

    if (addr < 0x0020) { // Write to an IO port
        cega_md_io_wr(addr, data);
    }
    else if (addr == 0x1100) {
        /* In order to safely write to the Z80 RAM, BUSREQ must be asserted.
           This ensures that the 68K has control of the bus, and the program
           can be written to Z80 RAM. When this is finished, the BUSREQ line
           may be released and the Z80 can operate as usual.
        */
        cega_z80_busreq(Z80ID, data & 0x01);
    }
    else if (addr == 0x1200) {
        /* Writing this address will set the Z80 RESET line. In this case, the
           line is asserted when the value written is low, and released when
           the value written is high. The value is inverted in this emulator
           for easier human understanding.
        */
        uint8_t val = (data & 0x01) == 0 ? 1 : 0; // Inverse
        uint8_t stat = cega_z80_reset_stat(Z80ID); // RESET line asserted?
        cega_z80_reset_line(Z80ID, val);

        // Reset the YM2612 when the Z80 reset line is freshly released
        if (!val && stat)
           ym2612_reset();
    }
    else {
        cega_log(CEGA_LOG_WRN, "ioctrl_wr08: %04x %02x\n", addr, data);
    }
}

static void cega_md_ioctrl_wr16(uint32_t addr, uint16_t data) {
    addr &= 0xfffe;

    /* When asserting/releasing the Z80's BUSREQ and/or RESET pins, simply
       shift the value by 8. The magic values are 0x0000 and 0x0100, (bit 8 set
       or unset) which become 0 and 1 when shifted and masked.
    */
    if (addr == 0x1100 || addr == 0x1200)
        cega_md_ioctrl_wr08(addr, data >> 8);
    else
        cega_log(CEGA_LOG_WRN, "ioctrl_wr16: %06x %04x\n", addr, data); // KLAX
}

uint8_t cega_md_m68k_rd08(uint32_t addr) {
    // Cart ROM
    if (addr < 0x400000) {
        switch (mdsys.sram_type) {
            default: case 0: {
                if (addr >= romsize)
                    return 0xff;
                break;
            }
            case 1: { // Traditional SRAM
                if (addr >= sramstart && addr <= sramend)
                    return mdsys.sram[addr & 0xffff];
                break;
            }
            case 2: { // Bank Swap SRAM
                if (mdsys.sram_lock && addr >= 0x200000 && addr <= 0x20ffff)
                    return mdsys.sram[addr & 0xffff];
                break;
            }
        }

        if (mdsys.mapper)
            return romdata[(addr & 0x7ffff) | (mdsys.mpage[addr >> 19] << 19)];
        else
            return romdata[addr];
    }
    // Z80 Area
    else if (addr > 0x9fffff && addr < 0xa10000) {
        /*| R | B | T |
          -------------
          | 0 | 0 | 0 |
          -------------
          | 0 | 1 | 1 |
          -------------
          | 1 | 0 | 0 |
          -------------
          | 1 | 1 | 0 |
          -------------
        */
        if (cega_z80_busack(Z80ID) && !cega_z80_reset_stat(Z80ID)) {
            return cega_md_z80_mem_rd(addr & 0x7fff);
        }
        else {
            cega_log(CEGA_LOG_DBG, "Bad Z80 Area Read08: %06x %d %d\n", addr,
                cega_z80_reset_stat(Z80ID), cega_z80_busack(Z80ID));
            return 0x00;
        }
    }
    // IO/Control Area
    else if (addr > 0xa0ffff && addr < 0xa12000) {
        return cega_md_ioctrl_rd08(addr);
    }
    // VDP
    else if (addr > 0xbfffff && addr < 0xe00000) {
        return mdvdp_rd08(addr);
    }
    // Work RAM
    else if (addr > 0xdfffff) {
        return mdsys.ram[addr & 0xffff];
    }

    cega_log(CEGA_LOG_WRN, "m68k_rd08: %06x\n", addr);
    return 0xff;
}

uint16_t cega_md_m68k_rd16(uint32_t addr) {
    // Cart ROM
    if (addr < 0x400000) {
        switch (mdsys.sram_type) {
            default: case 0: {
                if (addr >= romsize)
                    return 0xffff;
                break;
            }
            case 1: { // Traditional SRAM
                if (addr >= sramstart && addr <= sramend)
                    return mdsys.sram[addr - 0x20000];
                break;
            }
            case 2: { // Bank Swap SRAM
                if (mdsys.sram_lock && addr >= 0x200000 && addr <= 0x20ffff)
                    return mdsys.sram[addr & 0xffff];
                break;
            }
        }

        if (mdsys.mapper)
            return (cega_md_m68k_rd08(addr) << 8) | cega_md_m68k_rd08(addr + 1);
        else
            return (romdata[addr] << 8) | romdata[addr + 1];
    }
    // Z80 Area
    else if (addr > 0x9fffff && addr < 0xa10000) {
        if (cega_z80_busack(Z80ID) && !cega_z80_reset_stat(Z80ID)) {
            uint8_t val = cega_md_z80_mem_rd(addr & 0x7fff);
            return (val << 8) | val; // Just double it up
        }
        else {
            cega_log(CEGA_LOG_DBG, "Bad Z80 Area Read16: %06x\n", addr);
            return 0x00;
        }
    }
    // IO/Control Area
    else if (addr > 0xa0ffff && addr < 0xa12000) {
        return cega_md_ioctrl_rd16(addr);
    }
    // VDP
    else if (addr > 0xbfffff && addr < 0xe00000) {
        return mdvdp_rd16(addr);
    }
    // Work RAM
    else if (addr > 0xdfffff) {
        return (mdsys.ram[addr & 0xffff] << 8) | mdsys.ram[(addr & 0xffff) + 1];
    }

    cega_log(CEGA_LOG_WRN, "m68k_rd16: %06x\n", addr);
    return 0xffff;
}

void cega_md_m68k_wr08(uint32_t addr, uint8_t data) {
    if (addr < 0x400000) {
        switch (mdsys.sram_type) {
            default: case 0: {
                break;
            }
            case 1: { // Traditional SRAM
                if (addr >= sramstart && addr <= sramend)
                    mdsys.sram[addr & 0xffff] = data;
                break;
            }
            case 2: { // Bank Swap SRAM
                if (mdsys.sram_lock && addr >= 0x200000 && addr <= 0x20ffff)
                    mdsys.sram[addr & 0xffff] = data;
                break;
            }
        }
    }
    // Z80 Area
    else if (addr > 0x9fffff && addr < 0xa10000) {
        if (cega_z80_busack(Z80ID) && !cega_z80_reset_stat(Z80ID)) {
            cega_md_z80_mem_wr(addr & 0x7fff, data);
            return;
        }
        cega_log(CEGA_LOG_DBG, "Bad Z80 Area Write08: %06x %02x\n", addr, data);
    }
    // IO
    else if (addr > 0xa0ffff && addr < 0xa12000) {
        cega_md_ioctrl_wr08(addr, data);
    }
    // SRAM Lock
    else if (addr == 0xa130f1) {
        mdsys.sram_lock = data & 0x01;
        mdsys.sram_type = 2;
    }
    else if (mdsys.mapper && addr > 0xa130f1 && addr < 0xa13100) { // SSF2
        /* Bank select for SSF2 Mapper - Cartridge pages 0 to 63 mapped to
           memory pages 1-7. Memory Page 0 never switches.
        */
        switch (addr) {
            case 0xa130f3: mdsys.mpage[1] = data & 0x3f; break;
            case 0xa130f5: mdsys.mpage[2] = data & 0x3f; break;
            case 0xa130f7: mdsys.mpage[3] = data & 0x3f; break;
            case 0xa130f9: mdsys.mpage[4] = data & 0x3f; break;
            case 0xa130fb: mdsys.mpage[5] = data & 0x3f; break;
            case 0xa130fd: mdsys.mpage[6] = data & 0x3f; break;
            case 0xa130ff: mdsys.mpage[7] = data & 0x3f; break;
        }
    }
    // VDP
    else if (addr > 0xbfffff && addr < 0xe00000) {
        // The PSG is integrated into the VDP on the Mega Drive
        if (addr > 0xc00010 && addr < 0xc00018)
            sn76496_wr(&psg, data);
        /*else
            mdvdp_wr08(addr, data);*/
    }
    // Work RAM
    else if (addr > 0xdfffff) {
        mdsys.ram[addr & 0xffff] = data;
    }
}

void cega_md_m68k_wr16(uint32_t addr, uint16_t data) {
    if (addr < 0x400000) {
        //cega_log(CEGA_LOG_WRN, "rom write %06x %04x\n", addr, data);
    }
    // Z80 Area
    else if (addr > 0x9fffff && addr < 0xa10000) {
        if (cega_z80_busack(Z80ID) && !cega_z80_reset_stat(Z80ID)) {
            cega_md_z80_mem_wr(addr & 0x7fff, (data >> 8) & 0xff);
            return;
        }
        cega_log(CEGA_LOG_DBG, "Bad Z80 Area Write16: %06x %02x\n", addr, data);
    }
    // IO
    else if (addr > 0xa0ffff && addr < 0xa12000) {
         cega_md_ioctrl_wr16(addr, data);
    }
    // SRAM Lock
    else if (addr == 0xa130f1) { // Does this ever get used? FIXME
        mdsys.sram_lock = data & 0x01;
        mdsys.sram_type = 2;
        cega_log(CEGA_LOG_WRN, "wr16 sram lock: %02x\n", data);
    }
    // VDP
    else if (addr > 0xbfffff && addr < 0xe00000) {
        if ((addr > 0xc00010) && (addr < 0xc00018))
            sn76496_wr(&psg, data & 0xff);
        else
            mdvdp_wr16(addr, data);
    }
    // Work RAM
    else if (addr > 0xdfffff) {
        data = swapb16(data);
        addr &= 0xffff;
        mdsys.ram[addr] = data & 0xff;
        mdsys.ram[addr + 1] = data >> 8;
    }
}

// Load a ROM Image
static int cega_md_rom_load(void *data, size_t size) {
    romdata = (uint8_t*)data; // Assign internal ROM pointer
    romsize = size; // Size of the ROM data in bytes

    // Populate ROM Header Info
    memcpy((char*)romheader.systype, (uint8_t*)romdata + 0x100, 16);
    memcpy((char*)romheader.copyright, (uint8_t*)romdata + 0x110, 16);
    memcpy((char*)romheader.title[0], (uint8_t*)romdata + 0x120, 48);
    memcpy((char*)romheader.title[1], (uint8_t*)romdata + 0x150, 48);
    memcpy((char*)romheader.serial, (uint8_t*)romdata + 0x180, 14);
    romheader.checksum = (romdata[0x18e] << 8) | romdata[0x18f];
    memcpy((uint8_t*)romheader.devices, (uint8_t*)romdata + 0x190, 16);
    memcpy((uint8_t*)romheader.range_rom, (uint8_t*)romdata + 0x1a0, 8);
    memcpy((uint8_t*)romheader.range_ram, (uint8_t*)romdata + 0x1a8, 8);
    memcpy((uint8_t*)romheader.extram, (uint8_t*)romdata + 0x1b0, 12);
    memcpy((char*)romheader.modem, (uint8_t*)romdata + 0x1bc, 12);
    memcpy((char*)romheader.region, (uint8_t*)romdata + 0x1f0, 3);

    cega_log(CEGA_LOG_DBG, "System Type: %s\n", romheader.systype);
    cega_log(CEGA_LOG_DBG, "Copyright: %s\n", romheader.copyright);
    cega_log(CEGA_LOG_DBG, "Title (Domestic): %s\n", romheader.title[0]);
    cega_log(CEGA_LOG_DBG, "Title (Overseas): %s\n", romheader.title[1]);
    cega_log(CEGA_LOG_DBG, "Serial: %s\n", romheader.serial);
    cega_log(CEGA_LOG_DBG, "Checksum: %04x\n", romheader.checksum);
    cega_log(CEGA_LOG_DBG, "Size (Bytes): %ld\n", romsize);

    /* Region Detection
    =========================================================================
    |        3        |        2        |        1        |        0        |
    =========================================================================
    |  Overseas 50Hz  |  Overseas 60Hz  |     Invalid     |  Domestic 60Hz  |
    -------------------------------------------------------------------------
    |  EU/Oceania/HK  |  Americas/PAL-M |                 | Japan/Korea/TW  |
    -------------------------------------------------------------------------
       Notes: This is the region format used in late releases, where an ASCII
              character representes a hexadecimal value. The original format
              uses ASCII letters J, U, and E to set valid regions. The code
              below converts the original format to this one.
    */
    uint8_t region = 0;

    // Original Format
    if (strchr(romheader.region, 'J'))
        region |= 0x01;
    if (strchr(romheader.region, 'U'))
        region |= 0x04;
    if (strchr(romheader.region, 'E'))
        region |= 0x08;

    // New Format
    if (region == 0) {
        char temp[2] = { romheader.region[0], '\0' };
        region = (uint8_t)strtol(temp, NULL, 16);
    }

    switch (region & 0x0d) {
        case 0x0d: {
            cega_log(CEGA_LOG_DBG, "Region: World (JUE)\n");
            if (!mdsys.region)
                mdsys.region = REGION_US;
            break;
        }
        case 0x0c: {
            cega_log(CEGA_LOG_DBG, "Region: Americas/Europe (UE)\n");
            if (mdsys.region == REGION_JP || !mdsys.region)
                mdsys.region = REGION_US;
            break;
        }
        case 0x09: {
            cega_log(CEGA_LOG_DBG, "Region: Japan/Europe (JE)\n");
            if (mdsys.region == REGION_US || !mdsys.region)
                mdsys.region = REGION_JP;
            break;
        }
        case 0x08: {
            cega_log(CEGA_LOG_DBG, "Region: PAL (E)\n");
            mdsys.region = REGION_EU;
            break;
        }
        case 0x05: {
            cega_log(CEGA_LOG_DBG, "Region: NTSC (JU)\n");
            if (mdsys.region == REGION_EU || !mdsys.region)
                mdsys.region = REGION_US;
            break;
        }
        case 0x04: {
            cega_log(CEGA_LOG_DBG, "Region: NTSC (U)\n");
            mdsys.region = REGION_US;
            break;
        }
        case 0x01: {
            cega_log(CEGA_LOG_DBG, "Region: NTSC (J)\n");
            mdsys.region = REGION_JP;
            break;
        }
        case 0x00: default: {
            cega_log(CEGA_LOG_DBG, "Region: Ambiguous\n");
            if (!mdsys.region)
                mdsys.region = REGION_US;
            break;
        }
    }

    if (romheader.extram[0] == 0x52 && romheader.extram[1] == 0x41) { // "RA"
        switch (romheader.extram[2]) {
            case 0xa0:
                cega_log(CEGA_LOG_DBG, "SRAM: No Save, 16-bit\n");
                break;
            case 0xb0:
                cega_log(CEGA_LOG_DBG, "SRAM: No Save, 8-bit Even\n");
                break;
            case 0xb8:
                cega_log(CEGA_LOG_DBG, "SRAM: No Save, 8-bit Odd\n");
                break;
            case 0xe0:
                cega_log(CEGA_LOG_DBG, "SRAM: 16-bit\n");
                mdsys.sram_type = 1;
                break;
            case 0xf0:
                cega_log(CEGA_LOG_DBG, "SRAM: 8-bit Even\n");
                mdsys.sram_type = 1;
                break;
            case 0xf8:
                cega_log(CEGA_LOG_DBG, "SRAM: 8-bit Odd\n");
                mdsys.sram_type = 1;
                break;
            default:
                cega_log(CEGA_LOG_DBG, "SRAM: Invalid Type %02x\n",
                    romheader.extram[2]);
                break;
        }

        sramstart = (romheader.extram[5] << 16) |
            (romheader.extram[6] << 8) |
            romheader.extram[7];
        cega_log(CEGA_LOG_DBG, "SRAM Start: %06x\n", sramstart);

        sramend = (romheader.extram[9] << 16) |
            (romheader.extram[10] << 8) |
            romheader.extram[11];
        cega_log(CEGA_LOG_DBG, "SRAM End: %06x\n", sramend);
    }

    /* If a ROM is larger than 4MB, a mapper is required. If the header
       contains the string "SEGA SSF" in the system type field, this also
       indicates that a mapper is required. Only one mapper was used in a
       licensed release (Super Street Fighter II). This is the mapper often
       used in large homebrew ROMs, though some other mappers exist for more
       recent, unlicensed releases. Currently, only the SSF2 mapper is
       implemented.
    */
    if (romsize > 4194304 || strstr(romheader.systype, "SEGA SSF"))
        mdsys.mapper = MD_MAPPER_SSF;

    return 1;
}

void cega_md_init(void) {
    // Seed the random number generator
    srand(time(NULL));

    // Initialize SRAM
    for (int i = 0; i < SIZE_64K; ++i)
        mdsys.sram[i] = 0xff;

    // Set function pointer for frame execution
    cega_exec = &cega_md_exec;

    // Set function pointers for Z80 I/O and Memory Reads/Writes
    cega_z80_port_rd[Z80ID] = &cega_md_z80_io_rd;
    cega_z80_port_wr[Z80ID] = &cega_md_z80_io_wr;
    cega_z80_mem_rd[Z80ID] = &cega_md_z80_mem_rd;
    cega_z80_mem_wr[Z80ID] = &cega_md_z80_mem_wr;

    // Set function pointer for ROM loading
    cega_rom_load = &cega_md_rom_load;

    // Set function pointers for states
    cega_state_size = &cega_md_state_size;
    cega_state_load = &cega_md_state_load;
    cega_state_load_raw = &cega_md_state_load_raw;
    cega_state_save = &cega_md_state_save;
    cega_state_save_raw = &cega_md_state_save_raw;

    // Set function pointers for SRAM
    cega_sram_load = &cega_md_sram_load;
    cega_sram_save = &cega_md_sram_save;

    // Initialize the emulated chips
    cega_z80_init(Z80ID);
    cega_m68k_init();
    cega_mixer_chips_enable(1, 1);
    cega_md_io_init();

    // Set up ROM Pages
    for (int i = 0; i < 8; ++i)
        mdsys.mpage[i] = i;

    mdvdp_init();

    // Initialize PSG
    sn76496_init(&psg, 0, NTAP, NSHIFT);
    cega_mixer_set_psg(&psg);
}

void cega_md_deinit(void) {
}

void cega_md_reset(void) {
    cega_z80_reset(Z80ID);
    cega_m68k_reset();
    mdvdp_reset();

    // Reinitialize PSG
    sn76496_init(&psg, 0, NTAP, NSHIFT);
}

// Load SRAM
int cega_md_sram_load(const char *filename) {
    FILE *file;
    size_t filesize, result;

    // Open the file for reading
    file = fopen(filename, "rb");
    if (!file)
        return 2;

    // Find out the file's size
    fseek(file, 0, SEEK_END);
    filesize = ftell(file);
    fseek(file, 0, SEEK_SET);

    if (filesize > SIZE_64K) {
        fclose(file);
        return 0;
    }

    // Read the file into the system's SRAM slot and then close it
    result = fread(mdsys.sram, sizeof(uint8_t), filesize, file);
    if (result != filesize) {
        fclose(file);
        return 0;
    }

    fclose(file);
    return 1; // Success!
}

// Save SRAM
int cega_md_sram_save(const char *filename) {
    if (!mdsys.sram_type)
        return 2;

    FILE *file;
    file = fopen(filename, "wb");
    if (!file)
        return 0;

    // Write and close the file
    fwrite(mdsys.sram, SIZE_64K, sizeof(uint8_t), file);
    fclose(file);

    return 1; // Success!
}

// Get the Region for the Mega Drive
uint8_t cega_md_get_region(void) {
    return mdsys.region;
}

// Set the Region for the Mega Drive
void cega_md_set_region(uint8_t region) {
    /* Version Register
    =========================================================================
    |    7   |    6   |    5   |    4   |    3   |    2   |    1   |    0   |
    =========================================================================
    | Export |   PAL  | No MCD |    0   |    0   |    0   |    0   |    1   |
    -------------------------------------------------------------------------
       Notes: Bit 0 is always 1, Bits 0-4 are always 0. When there is a CD
              unit attached, Bit 5 will be 0, otherwise it is 1. Bit 6 is set
              for PAL systems, Export is set for non-Japanese systems.
    */
    if (region == REGION_EU) {
        mcpf = MCYC_PER_FRAME_PAL;
        mdsys.version = 0xe1; // Export, PAL, No MCD
    }
    else if (region == REGION_JP) {
        mcpf = MCYC_PER_FRAME_NTSC;
        mdsys.version = 0x21; // Domestic, NTSC, No MCD
    }
    else {
        mcpf = MCYC_PER_FRAME_NTSC;
        mdsys.version = 0xa1; // Export, NTSC, No MCD
    }

    mdsys.region = region;
}

// Catch the VDP up to the CPU
void cega_md_vdpsync(void) {
    if (vdpsynced) return;

    // Master Cycles to run
    size_t runcycs = (icycs * DIV_M68K);

    for (size_t v = 0; v < runcycs; ++v) {
        if (++vcycs == 4) {
            vcycs = 0;
            if (mdvdp_exec()) {
                frameready = 1; // Frame is ready
                vstored = runcycs - v - 1;
                break;
            }
        }
    }

    // VDP has been synced
    vdpsynced = 1;
}

// Run emulation for one frame
void cega_md_exec(void) {
    // Run the VDP for enough cycles to catch up to where it should be
    frameready = 0;
    for (size_t v = 0; v < vstored; ++v) {
        if (++vcycs == 4) {
            vcycs = 0;
            frameready = mdvdp_exec();
        }
    }

    while (!frameready) {
        icycs = cega_m68k_exec();
        mcycs += icycs * DIV_M68K;

        while (zcycs < mcycs) {
            cega_z80_exec(Z80ID);
            zcycs += DIV_Z80;

            for (size_t s = 0; s < DIV_Z80; ++s) {
                if (++psgcycs == MCYC_PSG) {
                    psgcycs = 0;
                    sn76496_exec(&psg);
                }
                if (++ymcycs == MCYC_YM2612) {
                    ymcycs = 0;
                    ymsamps += ymfm_shim_exec();
                }
            }
        }

        if (!vdpsynced)
            cega_md_vdpsync();

        vdpsynced = 0;
    }

    // Store spare cycles
    mcycs -= mcpf;
    zcycs -= mcpf;

    // Resample audio and push to the frontend
    cega_mixer_resamp_md(psg.bufpos, ymsamps);

    // Set sample counter to 0
    ymsamps = 0;
}

size_t cega_md_state_size(void) { return 0; }

int cega_md_state_load_raw(const void *sstate, size_t sz) {
    (void)sstate; (void)sz; return 0;
}

int cega_md_state_load(const char *filename) { if (filename) { } return 0; }
const void* cega_md_state_save_raw(void) { return NULL; }
int cega_md_state_save(const char *filename) { if (filename) { } return 0; }
