/*
 * 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/.
 */

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

#include "cega.h"
#include "cega_serial.h"
#include "cega_z80.h"

#define CHIPS_IMPL
#include "z80.h"

static z80_t z80ctx;
static uint64_t delaycycs = 0;
static uint64_t pins = 0;
static uint8_t intvec = 0;
static uint8_t busreq = 0;
static uint8_t busack = 0;
static uint8_t busack_new = 0;
static uint8_t busack_timer = 0;
static uint8_t reset = 0;

// Initialize the Z80
void cega_z80_init(void) {
    pins = z80_init(&z80ctx);

    // On the Mega Drive, the BUSREQ and RESET lines are asserted on boot
    if (cega_get_system() == SYSTEM_MD) {
        busack = 1;
        busreq = 1;
        reset = 1;
    }

    // SMS Startup Values
    z80ctx.sp = 0xdff0;
    z80ctx.ir = 0x0009;
    z80ctx.af = 0x0040;
    z80ctx.bc = 0x0000;
    z80ctx.de = 0x0000;
    z80ctx.hl = 0x0000;
    z80ctx.ix = 0x0000;
    z80ctx.iy = 0x0000;
    z80ctx.af2 = 0x0000;
    z80ctx.bc2 = 0x0000;
    z80ctx.de2 = 0x0000;
    z80ctx.hl2 = 0x0000;
}

// Assert the IRQ Line
void cega_z80_irq(uint8_t data) {
    pins |= Z80_INT;
    intvec = data;
}

// Clear the IRQ Line
void cega_z80_irq_clr(void) {
    pins &= ~Z80_INT;
}

// Read the value of the BUSACK Line
uint8_t cega_z80_busack(void) {
    return busack;
}

// Set the BUSREQ Line
void cega_z80_busreq(uint8_t line) {
    busreq = line;
    busack_timer = 2;

    if (busreq && !reset)
        busack_new = 1;
    else
        busack_new = 0;
}

// Assert the NMI Line
void cega_z80_nmi(void) {
    pins |= Z80_NMI;
}

// Clear the NMI Line
void cega_z80_nmi_clr(void) {
    pins &= ~Z80_NMI;
}

// Issue a Reset
void cega_z80_reset(void) {
    pins = z80_prefetch(&z80ctx, 0x0000);
    reset = 0;
    z80ctx.im = 0;
    z80ctx.ir = 0;
    z80ctx.iff1 = 0;
    z80ctx.iff2 = 0;
}

// Read the value of the RESET Line
uint8_t cega_z80_reset_stat(void) {
    return reset;
}

// Assert or clear the RESET Line
void cega_z80_reset_line(uint8_t data) {
    /* If the line is already in the desired state, go no further. If the line
       is being asserted or deasserted, change the status.
    */
    if (reset == data)
        return;
    else
        reset = data;

    /* Perform the reset if the line was freshly asserted -- When the line is
       no longer asserted, the Z80 will begin executing opcodes at 0x0000.
    */
    if (data)
        cega_z80_reset();
}

// Delay by n cycles
void cega_z80_delay(uint64_t n) {
    delaycycs += n;
}

uint32_t cega_z80_exec(void) {
    if (delaycycs) {
        --delaycycs;
        return 1;
    }

    if (busack_timer) {
        --busack_timer;
        if (!busack_timer)
            busack = busack_new;
    }

    if (busack || reset)
        return 1;

    pins = z80_tick(&z80ctx, pins);
    if (pins & Z80_MREQ) {
        const uint16_t addr = Z80_GET_ADDR(pins);
        if (pins & Z80_RD) {
            uint8_t data = cega_mem_rd(addr);
            Z80_SET_DATA(pins, data);
        }
        else if (pins & Z80_WR) {
            uint8_t data = Z80_GET_DATA(pins);
            cega_mem_wr(addr, data);
        }
    }
    else if (pins & Z80_IORQ) {
        const uint16_t port = Z80_GET_ADDR(pins);
        if (pins & Z80_M1) {
            Z80_SET_DATA(pins, intvec);
        }
        if (pins & Z80_RD) {
            uint8_t data = cega_port_rd(port & 0xff);
            Z80_SET_DATA(pins, data);
        }
        else if (pins & Z80_WR) {
            uint8_t data = Z80_GET_DATA(pins);
            cega_port_wr(port & 0xff, data);
        }
    }

    return 1;
}

void cega_z80_state_load(uint8_t *st) {
    z80ctx.step = cega_serial_pop16(st);
    z80ctx.addr = cega_serial_pop16(st);
    z80ctx.dlatch = cega_serial_pop8(st);
    z80ctx.opcode = cega_serial_pop8(st);
    z80ctx.hlx_idx = cega_serial_pop8(st);
    z80ctx.prefix_active = cega_serial_pop8(st);
    z80ctx.pins = cega_serial_pop64(st);
    z80ctx.int_bits = cega_serial_pop64(st);
    z80ctx.pc = cega_serial_pop16(st);
    z80ctx.af = cega_serial_pop16(st);
    z80ctx.bc = cega_serial_pop16(st);
    z80ctx.de = cega_serial_pop16(st);
    z80ctx.hl = cega_serial_pop16(st);
    z80ctx.ix = cega_serial_pop16(st);
    z80ctx.iy = cega_serial_pop16(st);
    z80ctx.wz = cega_serial_pop16(st);
    z80ctx.sp = cega_serial_pop16(st);
    z80ctx.ir = cega_serial_pop16(st);
    z80ctx.af2 = cega_serial_pop16(st);
    z80ctx.bc2 = cega_serial_pop16(st);
    z80ctx.de2 = cega_serial_pop16(st);
    z80ctx.hl2 = cega_serial_pop16(st);
    z80ctx.im = cega_serial_pop8(st);
    z80ctx.iff1 = cega_serial_pop8(st);
    z80ctx.iff2 = cega_serial_pop8(st);
    delaycycs = cega_serial_pop64(st);
    pins = cega_serial_pop64(st);
    intvec = cega_serial_pop8(st);
    busack = cega_serial_pop8(st);
    busack_new = cega_serial_pop8(st);
    busack_timer = cega_serial_pop8(st);
    busreq = cega_serial_pop8(st);
    reset = cega_serial_pop8(st);
}

void cega_z80_state_save(uint8_t *st) {
    cega_serial_push16(st, z80ctx.step);
    cega_serial_push16(st, z80ctx.addr);
    cega_serial_push8(st, z80ctx.dlatch);
    cega_serial_push8(st, z80ctx.opcode);
    cega_serial_push8(st, z80ctx.hlx_idx);
    cega_serial_push8(st, z80ctx.prefix_active);
    cega_serial_push64(st, z80ctx.pins);
    cega_serial_push64(st, z80ctx.int_bits);
    cega_serial_push16(st, z80ctx.pc);
    cega_serial_push16(st, z80ctx.af);
    cega_serial_push16(st, z80ctx.bc);
    cega_serial_push16(st, z80ctx.de);
    cega_serial_push16(st, z80ctx.hl);
    cega_serial_push16(st, z80ctx.ix);
    cega_serial_push16(st, z80ctx.iy);
    cega_serial_push16(st, z80ctx.wz);
    cega_serial_push16(st, z80ctx.sp);
    cega_serial_push16(st, z80ctx.ir);
    cega_serial_push16(st, z80ctx.af2);
    cega_serial_push16(st, z80ctx.bc2);
    cega_serial_push16(st, z80ctx.de2);
    cega_serial_push16(st, z80ctx.hl2);
    cega_serial_push8(st, z80ctx.im);
    cega_serial_push8(st, z80ctx.iff1);
    cega_serial_push8(st, z80ctx.iff2);
    cega_serial_push64(st, delaycycs);
    cega_serial_push64(st, pins);
    cega_serial_push8(st, intvec);
    cega_serial_push8(st, busack);
    cega_serial_push8(st, busack_new);
    cega_serial_push8(st, busack_timer);
    cega_serial_push8(st, busreq);
    cega_serial_push8(st, reset);
}
