/*
 * 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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

#include "cega.h"
#include "cega_md.h"
#include "cega_sg.h"
#include "cega_sms.h"
#include "cega_mixer.h"
#include "cega_z80.h"
#include "mdvdp.h"
#include "smsvdp.h"
#include "sn76496.h"
#include "ym2413.h"
#include "ymfm_opn.h"
#include "ymfm_shim.h"

// Function Pointers for Memory and IO Operations
uint8_t (*cega_port_rd)(uint8_t);
void (*cega_port_wr)(uint8_t, uint8_t);
uint8_t (*cega_mem_rd)(uint16_t);
void (*cega_mem_wr)(uint16_t, uint8_t);

// Function Pointers for loading ROM Images
int (*cega_bios_load)(void*, size_t);
int (*cega_rom_load)(void*, size_t);

// Function Pointers for States
size_t (*cega_state_size)(void);
void (*cega_state_load_raw)(const void*);
int (*cega_state_load)(const char*);
const void* (*cega_state_save_raw)(void);
int (*cega_state_save)(const char*);

// Function Pointers for SRAM
int (*cega_sram_load)(const char*);
int (*cega_sram_save)(const char*);

// Log callback
void (*cega_log)(int, const char *, ...);

// Input poll callbacks
uint8_t (*cega_input_cb[NUMINPUTS_SMS])(int);
uint16_t (*cega_md_input_cb[NUMINPUTS_MD])(int);

// System being emulated
static int sys = 0;

// Set the log callback
void cega_log_set_callback(void (*cb)(int, const char *, ...)) {
    cega_log = cb;
}

// Set the Input Callback to allow the emulator to strobe the input state
void cega_input_set_callback(int port, uint8_t (*cb)(int)) {
    cega_input_cb[port] = cb;
}

void cega_md_input_set_callback(int port, uint16_t (*cb)(int)) {
    cega_md_input_cb[port] = cb;
}

// Set the region
void cega_set_region(uint8_t region) {
    switch (sys) {
        default:
        case SYSTEM_SG: {
            cega_mixer_set_region(region);
            smsvdp_set_region(region);
            break;
        }
        case SYSTEM_SMS: {
            cega_sms_set_region(region);
            cega_mixer_set_region(region);
            smsvdp_set_region(region);
            break;
        }
        case SYSTEM_GG: {
            // SYSTEM_GG is always 60Hz
            cega_sms_set_region(region > REGION_US ? REGION_US : region);
            break;
        }
        case SYSTEM_MD: {
            cega_md_set_region(region);
            cega_mixer_set_region(region);
            mdvdp_set_region(region);
            break;
        }
    }
}

// Set the system to be emulated
void cega_set_system(int system) {
    sys = system;
}

// Get the system currently being emulated
int cega_get_system(void) {
    return sys;
}

// Initialize
void cega_init(void) {
    switch (sys) {
        case SYSTEM_SG: {
            cega_sg_init();
            cega_mixer_init(0);
            sn76496_init(0, NTAP_SG, NSHIFT_SG);
            smsvdp_init();
            smsvdp_set_palette(1); // Always use TeaTime palette for SG-1000
            break;
        }
        case SYSTEM_SMS: {
            cega_sms_init();
            cega_mixer_init(0);
            sn76496_init(0, NTAP, NSHIFT);
            ym2413_init();
            smsvdp_init();
            break;
        }
        case SYSTEM_GG: {
            cega_sms_init(); // Many things are common to SMS and Game Gear
            cega_gg_init(); // Game Gear specific tasks
            cega_mixer_init(1);
            sn76496_init(1, NTAP, NSHIFT);
            smsvdp_init();
            break;
        }
        case SYSTEM_MD: {
            cega_md_init();
            cega_mixer_init(0);
            sn76496_init(0, NTAP, NSHIFT);
            ymfm_shim_init();
            break;
        }
    }

    cega_z80_init();
}

// Deinitialize
void cega_deinit(void) {
    switch (sys) {
        case SYSTEM_SG: {
            cega_sg_deinit();
            break;
        }
        case SYSTEM_SMS: {
            ym2413_deinit();
            cega_sms_deinit();
            break;
        }
        case SYSTEM_GG: {
            cega_sms_deinit();
            break;
        }
        case SYSTEM_MD: {
            cega_md_deinit();
            break;
        }
    }

    cega_mixer_deinit();
}

// Reset the system
void cega_reset(int hard) {
    if (hard) { } // Currently unused

    switch (sys) {
        case SYSTEM_SG: {
            cega_sg_reset();
            sn76496_init(0, NTAP_SG, NSHIFT_SG);
            smsvdp_init();
            break;
        }
        case SYSTEM_SMS: {
            cega_sms_reset();
            sn76496_init(0, NTAP, NSHIFT);
            ym2413_reset();
            smsvdp_init();
            break;
        }
        case SYSTEM_GG: {
            cega_sms_reset();
            sn76496_init(1, NTAP, NSHIFT);
            smsvdp_init();
            break;
        }
        case SYSTEM_MD: {
            cega_md_reset();
            sn76496_init(0, NTAP, NSHIFT);
            ym2612_reset();
            break;
        }
    }

    cega_z80_reset();
}

// Run emulation for one frame
void cega_exec(void) {
    unsigned frameready = 0;
    unsigned mcycs = 0;

    while (!frameready) {
        // Clock the CPU and PSG/FM chips every third master cycle
        if (mcycs % 3 == 0) {
            cega_z80_exec();
            cega_mixer_chips_exec();
        }

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

        ++mcycs;
    }

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