/*
 * Copyright (c) 2021-2025 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/.
 */

// SG-1000 (Sega Game 1000)

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

#include "cega.h"
#include "cega_mixer.h"
#include "cega_serial.h"
#include "cega_sg.h"
#include "cega_sms_io.h"
#include "cega_z80.h"
#include "smsvdp.h"
#include "sn76496.h"

#define Z80ID 0

#define SIZE_STATE 25827
#define STATE_VERSION 0xCE6A0001
static uint8_t state[SIZE_STATE];

static uint8_t *romdata = NULL; // Game ROM
static size_t romsize = 0; // Size of the ROM in bytes
static uint16_t expmask = 0x0000; // 2K or 8K mask for RAM expansion carts

static unsigned frameready = 0;
static unsigned mcycs = 0;

static sg_sys_t sgsys; // SG-1000 System Context
static sn76496_t psg; // Sega PSG Context

// Terebi Oekaki Input Callback
uint8_t (*cega_sg_terebi_cb)(uint8_t);

void cega_sg_terebi_set_callback(uint8_t (*cb)(uint8_t)) {
    cega_sg_terebi_cb = cb;
}

static uint8_t cega_sg_port_rd(uint8_t port) {
    if (port < 0xc0) {
        if (port & 0x01) // Odd returns VDP status
            return smsvdp_rd_stat();
        else // Even returns VDP data
            return smsvdp_rd_data();
    }
    else { // 0xc0 - 0xff: IO ports (Controllers)
        port &= 0x01;
        sgsys.ctrl[port] = cega_sms_io_rd(port);
        return sgsys.ctrl[port];
    }

    return 0xff;
}

static void cega_sg_port_wr(uint8_t port, uint8_t data) {
    if (port < 0x80) {
        sn76496_wr(&psg, data);
    }
    else if (port < 0xc0) { // 0x80 - 0xbf: VDP Data and Control ports
        if (port & 0x01) // Odd goes to control port
            smsvdp_wr_ctrl(data);
        else // Even goes to data port
            smsvdp_wr_data(data);
    }
}

/* Default SG-1000 Cartridges
 * Map from 0x0000 to the end of the ROM
*/
static uint8_t cega_sg_mem_rd(uint16_t addr) {
    if (addr < 0xc000) {
        if (addr >= romsize) // Return 0xff for reads past the ROM
            return 0xff;
        else
            return romdata[addr];
    }
    else if (addr >= 0xc000) {
        return sgsys.ram[addr & 0x3ff];
    }

    return 0xff;
}

static void cega_sg_mem_wr(uint16_t addr, uint8_t data) {
    if (addr >= 0xc000)
        sgsys.ram[addr & 0x3ff] = data;
}

/* RAM Expanded Carts
 * Cartridge ROM: 0x0000 - 0x7fff
 * Cartridge RAM: 0x8000 - 0xbfff
 * Note: Cartridges may use 2K or 8K RAM, and reads/writes are mirrored
 *  accordingly (2K mirroring or 8K mirroring of a 16K address space).
 */
static uint8_t cega_sg_mem_rd_ramexp(uint16_t addr) {
    if (addr < 0x8000)
        return romdata[addr];
    else if (addr < 0xc000)
        return sgsys.expram[addr & expmask];
    else if (addr >= 0xc000)
        return sgsys.ram[addr & 0x3ff];

    return 0xff;
}

static void cega_sg_mem_wr_ramexp(uint16_t addr, uint8_t data) {
    if (addr >= 0x8000 && addr < 0xc000)
        sgsys.expram[addr & expmask] = data;
    if (addr >= 0xc000)
        sgsys.ram[addr & 0x3ff] = data;
}

/* Terebi Oekaki
 * Map from 0x0000 to the end of the ROM
 * Note: This is mostly the same as a normal cartridge with the exception of
 *  special addresses to handle the Terebi Oekaki tablet.
*/
static uint8_t cega_sg_mem_rd_terebi(uint16_t addr) {
    if (addr < 0xc000) {
        if (addr == 0x8000) // Read "Pressed" Status
            return ~cega_sg_terebi_cb(SG_TEREBI_P) & 0x01;
        if (addr == 0xa000) // Read currently latched axis' position
            return cega_sg_terebi_cb(sgsys.tbaxis);
        else if (addr >= romsize) // Return 0xff for reads past the ROM
            return 0xff;
        else
            return romdata[addr];
    }
    else if (addr >= 0xc000) {
        return sgsys.ram[addr & 0x3ff];
    }

    return 0xff;
}

static void cega_sg_mem_wr_terebi(uint16_t addr, uint8_t data) {
    if (addr >= 0xc000)
        sgsys.ram[addr & 0x3ff] = data;
    else if (addr == 0x6000) // Latch the requested axis (inverse)
        sgsys.tbaxis = (data & SG_TEREBI_Y) ^ 1;
}

/* Taiwan MSX Type A RAM Expansion
 * Cartridge ROM: 0x0000 - 0x1fff
 * Expansion RAM: 0x2000 - 0x3fff
 * Cartridge ROM: 0x4000 - 0xbfff
 * Note: 8K RAM is mapped at 0x2000, and the remaining ROM is mapped at 0x4000.
 *  There is no masking required to access these higher ROM banks, and the dumps
 *  contain a block of 0xff in the space where Expansion RAM is mapped.
 */
static uint8_t cega_sg_mem_rd_twmsxa(uint16_t addr) {
    if (addr < 0x2000) // 8K Page 0
        return romdata[addr];
    else if (addr < 0x4000) // 8K Page 1
        return sgsys.expram[addr & 0x1fff];
    else if (addr < 0xc000) // 8K Pages 2-5
        return romdata[addr];
    else if (addr >= 0xc000)
        return sgsys.ram[addr & 0x3ff];

    return 0xff;
}

static void cega_sg_mem_wr_twmsxa(uint16_t addr, uint8_t data) {
    if (addr >= 0x2000 && addr < 0x4000)
        sgsys.expram[addr & 0x1fff] = data;
    if (addr >= 0xc000)
        sgsys.ram[addr & 0x3ff] = data;
}

/* Taiwan MSX Type B RAM Expansion
 * Cartridge ROM: 0x0000 - 0xbfff
 * Expansion RAM: 0xc000 - 0xffff (8K, mirrored)
 * Note: System RAM is replaced completely by Expansion RAM
 */
static uint8_t cega_sg_mem_rd_twmsxb(uint16_t addr) {
    if (addr < 0xc000) {
        if (addr >= romsize) // Return 0xff for reads past the ROM
            return 0xff;
        else
            return romdata[addr];
    }
    else if (addr >= 0xc000) {
        return sgsys.expram[addr & 0x1fff];
    }

    return 0xff;
}

static void cega_sg_mem_wr_twmsxb(uint16_t addr, uint8_t data) {
    if (addr >= 0xc000)
        sgsys.expram[addr & 0x1fff] = data;
}

// Load a ROM Image
static int cega_sg_rom_load(void *data, size_t size) {
    romdata = (uint8_t*)data; // Assign internal ROM pointer
    romsize = size; // Record the true size of the ROM data in bytes
    return 1;
}

// Initialize memory and set I/O states to default
void cega_sg_init(void) {
    // Fill RAM with garbage
    srand(time(NULL));
    for (int i = 0; i < SIZE_SGRAM; i++)
        sgsys.ram[i] = rand() % 256; // Random numbers from 0-255

    // Initialize the Z80
    cega_z80_init(Z80ID);

    // Initialize the VDP
    smsvdp_init();
    smsvdp_set_palette(1); // Always use TeaTime palette for SG-1000

    // Initialize controller port handlers
    cega_sms_io_init();

    // Set default controller port states
    sgsys.ctrl[0] = sgsys.ctrl[1] = 0xff;

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

    // Set function pointers for I/O and Memory Reads/Writes
    cega_z80_port_rd[Z80ID] = &cega_sg_port_rd;
    cega_z80_port_wr[Z80ID] = &cega_sg_port_wr;
    cega_z80_mem_rd[Z80ID] = &cega_sg_mem_rd;
    cega_z80_mem_wr[Z80ID] = &cega_sg_mem_wr;

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

    // Set function pointers for states
    cega_state_size = &cega_sg_state_size;
    cega_state_load = &cega_sg_state_load;
    cega_state_load_raw = &cega_sg_state_load_raw;
    cega_state_save = &cega_sg_state_save;
    cega_state_save_raw = &cega_sg_state_save_raw;

    // Set function pointers for SRAM
    cega_sram_load = &cega_sg_sram_load;
    cega_sram_save = &cega_sg_sram_save;

    // Initialize PSG
    sn76496_init(&psg, 0, NTAP_SG, NSHIFT_SG);
    cega_mixer_set_psg(&psg);
}

// Deinitialize any allocated memory
void cega_sg_deinit(void) {
}

void cega_sg_reset(void) {
    // Set default controller port states
    sgsys.ctrl[0] = sgsys.ctrl[1] = 0xff;

    // Reset the Z80
    cega_z80_reset(Z80ID);

    // Reset the VDP
    smsvdp_init();

    // Reinitialize PSG
    sn76496_init(&psg, 0, NTAP_SG, NSHIFT_SG);
}

void cega_sg_set_mapper(uint16_t m) {
    switch (m) {
        case SG_MAPPER_RAM2K: {
            cega_z80_mem_rd[Z80ID] = &cega_sg_mem_rd_ramexp;
            cega_z80_mem_wr[Z80ID] = &cega_sg_mem_wr_ramexp;
            expmask = SIZE_2K - 1;
            break;
        }
        case SG_MAPPER_RAM8K: {
            cega_z80_mem_rd[Z80ID] = &cega_sg_mem_rd_ramexp;
            cega_z80_mem_wr[Z80ID] = &cega_sg_mem_wr_ramexp;
            expmask = SIZE_8K - 1;
            break;
        }
        case SG_MAPPER_TEREBI: {
            cega_z80_mem_rd[Z80ID] = &cega_sg_mem_rd_terebi;
            cega_z80_mem_wr[Z80ID] = &cega_sg_mem_wr_terebi;
            break;
        }
        case SG_MAPPER_TWMSXA: {
            cega_z80_mem_rd[Z80ID] = &cega_sg_mem_rd_twmsxa;
            cega_z80_mem_wr[Z80ID] = &cega_sg_mem_wr_twmsxa;
            break;
        }
        case SG_MAPPER_TWMSXB: {
            cega_z80_mem_rd[Z80ID] = &cega_sg_mem_rd_twmsxb;
            cega_z80_mem_wr[Z80ID] = &cega_sg_mem_wr_twmsxb;
            break;
        }
    }
}

// Return the size of a state
size_t cega_sg_state_size(void) {
    return SIZE_STATE;
}

// Load raw state data into the running system
int cega_sg_state_load_raw(const void *sstate, size_t sz) {
    (void)sz;
    uint8_t *st = (uint8_t*)sstate;
    cega_serial_begin();

    if ((cega_serial_peek32(st) & 0xffff0000) == 0xCE6A0000)
        cega_serial_pop32(st); // Nothing needs to be done

    cega_serial_popblk(sgsys.ram, st, SIZE_SGRAM);
    cega_serial_popblk(sgsys.expram, st, SIZE_8K);
    sgsys.ctrl[0] = cega_serial_pop8(st);
    sgsys.ctrl[1] = cega_serial_pop8(st);
    sgsys.tbaxis = cega_serial_pop8(st);
    cega_mixer_set_cycs(cega_serial_pop8(st), 0);
    cega_z80_state_load(Z80ID, st);

    psg.clatch = cega_serial_pop8(st);
    for (size_t i = 0; i < 4; ++i)
        psg.attenuator[i] = cega_serial_pop8(st);
    for (size_t i = 0; i < 3; ++i)
        psg.frequency[i] = cega_serial_pop16(st);
    psg.noise = cega_serial_pop8(st);
    psg.lfsr = cega_serial_pop16(st);
    for (size_t i = 0; i < 4; ++i)
        psg.counter[i] = cega_serial_pop16(st);
    for (size_t i = 0; i < 4; ++i)
        psg.output[i] = cega_serial_pop16(st);
    psg.freqff = cega_serial_pop8(st);
    psg.stctrl = cega_serial_pop8(st);

    smsvdp_t *vdp = smsvdp_ctx();
    cega_serial_popblk(vdp->vram, st, SIZE_VRAM);
    cega_serial_popblk(vdp->cram, st, SIZE_CRAM);
    vdp->tbl_col = cega_serial_pop16(st);
    vdp->tbl_pgen = cega_serial_pop16(st);
    vdp->tbl_pname = cega_serial_pop16(st);
    vdp->tbl_sattr = cega_serial_pop16(st);
    vdp->tbl_spgen = cega_serial_pop16(st);
    vdp->line = cega_serial_pop16(st);
    vdp->cyc = cega_serial_pop16(st);
    vdp->dot = cega_serial_pop16(st);
    vdp->addr = cega_serial_pop16(st);
    vdp->code = cega_serial_pop8(st);
    for (unsigned i = 0; i < 16; ++i)
        vdp->ctrl[i] = cega_serial_pop8(st);
    vdp->stat = cega_serial_pop8(st);
    vdp->rdbuf = cega_serial_pop8(st);
    vdp->alatch = cega_serial_pop8(st);
    vdp->clatch = cega_serial_pop8(st);
    vdp->wlatch = cega_serial_pop8(st);
    vdp->hscroll = cega_serial_pop8(st);
    vdp->hcount = cega_serial_pop8(st);
    vdp->lcount = cega_serial_pop8(st);
    vdp->int_fr_pending = cega_serial_pop8(st);
    vdp->int_ln_pending = cega_serial_pop8(st);
    vdp->paused = cega_serial_pop8(st);
    smsvdp_rehash();
    return 1;
}

// Load a state from a file
int cega_sg_state_load(const char *filename) {
    FILE *file;
    size_t filesize, result;
    void *sstatefile;

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

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

    // Allocate memory to read the file into
    sstatefile = (void*)calloc(filesize, sizeof(uint8_t));
    if (sstatefile == NULL) {
        fclose(file);
        return 0;
    }

    // Read the file into memory and then close it
    result = fread(sstatefile, sizeof(uint8_t), filesize, file);
    if (result != filesize) {
        free(sstatefile);
        fclose(file);
        return 0;
    }
    fclose(file);

    // File has been read, now copy it into the emulator
    int ret = cega_sg_state_load_raw((const void*)sstatefile, filesize);

    // Free the allocated memory
    free(sstatefile);

    return ret;
}

// Snapshot the running state and return the address of the raw data
const void* cega_sg_state_save_raw(void) {
    cega_serial_begin();
    cega_serial_push32(state, STATE_VERSION);
    cega_serial_pushblk(state, sgsys.ram, SIZE_SGRAM);
    cega_serial_pushblk(state, sgsys.expram, SIZE_8K);
    cega_serial_push8(state, sgsys.ctrl[0]);
    cega_serial_push8(state, sgsys.ctrl[1]);
    cega_serial_push8(state, sgsys.tbaxis);
    uint8_t psgcycs;
    cega_mixer_get_cycs(&psgcycs, NULL);
    cega_serial_push8(state, psgcycs);
    cega_z80_state_save(Z80ID, state);

    cega_serial_push8(state, psg.clatch);
    for (size_t i = 0; i < 4; ++i)
        cega_serial_push8(state, psg.attenuator[i]);
    for (size_t i = 0; i < 3; ++i)
        cega_serial_push16(state, psg.frequency[i]);
    cega_serial_push8(state, psg.noise);
    cega_serial_push16(state, psg.lfsr);
    for (size_t i = 0; i < 4; ++i)
        cega_serial_push16(state, psg.counter[i]);
    for (size_t i = 0; i < 4; ++i)
        cega_serial_push16(state, psg.output[i]);
    cega_serial_push8(state, psg.freqff);
    cega_serial_push8(state, psg.stctrl);

    smsvdp_t *vdp = smsvdp_ctx();
    cega_serial_pushblk(state, vdp->vram, SIZE_VRAM);
    cega_serial_pushblk(state, vdp->cram, SIZE_CRAM);
    cega_serial_push16(state, vdp->tbl_col);
    cega_serial_push16(state, vdp->tbl_pgen);
    cega_serial_push16(state, vdp->tbl_pname);
    cega_serial_push16(state, vdp->tbl_sattr);
    cega_serial_push16(state, vdp->tbl_spgen);
    cega_serial_push16(state, vdp->line);
    cega_serial_push16(state, vdp->cyc);
    cega_serial_push16(state, vdp->dot);
    cega_serial_push16(state, vdp->addr);
    cega_serial_push8(state, vdp->code);
    for (unsigned i = 0; i < 16; ++i)
        cega_serial_push8(state, vdp->ctrl[i]);
    cega_serial_push8(state, vdp->stat);
    cega_serial_push8(state, vdp->rdbuf);
    cega_serial_push8(state, vdp->alatch);
    cega_serial_push8(state, vdp->clatch);
    cega_serial_push8(state, vdp->wlatch);
    cega_serial_push8(state, vdp->hscroll);
    cega_serial_push8(state, vdp->hcount);
    cega_serial_push8(state, vdp->lcount);
    cega_serial_push8(state, vdp->int_fr_pending);
    cega_serial_push8(state, vdp->int_ln_pending);
    cega_serial_push8(state, vdp->paused);

    return (const void*)state;
}

// Save a state to a file
int cega_sg_state_save(const char *filename) {
    // Open the file for writing
    FILE *file;
    file = fopen(filename, "wb");
    if (!file)
        return 0;

    // Snapshot the running state and get the memory address
    uint8_t *sstate = (uint8_t*)cega_sg_state_save_raw();

    // Write and close the file
    fwrite(sstate, cega_state_size(), sizeof(uint8_t), file);
    fclose(file);

    return 1; // Success!
}

// Load SRAM
int cega_sg_sram_load(const char *filename) {
    if (filename) { } // Unused
    return 2;
}

// Save SRAM
int cega_sg_sram_save(const char *filename) {
    if (filename) { } // Unused
    return 2;
}

// Run emulation for one frame
void cega_sg_exec(void) {
    while (!frameready) {
        // Clock the CPU and PSG/FM chips every third master cycle
        if (mcycs % 3 == 0) {
            cega_z80_exec(Z80ID);
            cega_mixer_chips_exec();
        }

        // Clock the VDP every second master cycle
        if (mcycs % 2 == 0) {
            if (smsvdp_exec())
                frameready = 1; // Frame is ready
        }

        ++mcycs;
    }

    frameready = 0;

    // Resample audio and push to the frontend
    cega_mixer_resamp();
}
