/*
Copyright (c) 2020-2025 Rupert Carmichael
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

// VTech CreatiVision (also known as Dick Smith Wizzard, FunVision)

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

#include "jollycv.h"

#include "jcv_crvision.h"
#include "jcv_mixer.h"
#include "jcv_m6502.h"
#include "jcv_serial.h"

#include "tms9918.h"

#define DIV_PSG 16          // PSG Clock Divider
#define M6502_CYC_LINE 128  // 6502 CPU cycles per scanline (127.79553)
#define NUM_SCANLINES 313   // Total number of video scanlines

#define SIZE_STATE 17501
static uint8_t state[SIZE_STATE];
static uint32_t state_version = ('J' << 24) | ('C' << 16) | ('V' << 8) | 0x00;

static sn76489_t psg;           // PSG Context

static uint8_t ram[SIZE_CRVRAM];    // System RAM
static uint8_t *biosdata = NULL;    // BIOS ROM
static uint8_t *romdata = NULL;     // Game ROM
static size_t romsize = 0;          // Size of the ROM in bytes

// Frame execution related variables
static size_t psgcycs = 0;

// Stub implementation of the 6822 PIA
typedef struct _pia_t {
    uint8_t ddr[2]; // Data Direction Register
    uint8_t or[2]; // Output Register
    uint8_t cr[2]; // Control Register
} pia_t;
static pia_t pia;

// Mapper callbacks
static uint8_t (*jcv_crvision_rom_rd40)(uint16_t);
static uint8_t (*jcv_crvision_rom_rd80)(uint16_t);

// Pointers for 18K ROMs (Chopper Rescue)
static uint8_t *rombank1_8k = NULL; // First 8K bank
static uint8_t *rombank2_8k = NULL; // Second 8K bank
static uint8_t *rombank_2k = NULL;  // 2K bank

// Input callback
static unsigned (*jcv_crvision_input_cb)(const void*, int); // Input callback
static void *udata_input = NULL; // Input callback userdata

static unsigned crv_input_map[] = {
    // Common: Up, Down, Left, Right, Fire1, Fire2
    0x08, 0x02, 0x20, 0x04, 0x80, 0x80,

    // Left Pad: 1, 2, 3, 4, 5, 6
    0x0c, 0x30, 0x60, 0x28, 0x48, 0x50,
    // Left Pad: Cntl, Q, W, E, R, T
    0x80, 0x18, 0x0c, 0x14, 0x24, 0x44,
    // Left Pad: Backspace, A, S, D, F, G
    0x09, 0x11, 0x21, 0x41, 0x03, 0x05,
    // Left Pad: Shift, Z, X, C, V, B
    0x80, 0x0a, 0x12, 0x22, 0x42, 0x06,

    // Right Pad: 7, 8, 9, 0, :, -
    0x06, 0x42, 0x22, 0x12, 0x0a, 0x80,
    // Right Pad: Y, U, I, O, P, Retn
    0x05, 0x03, 0x41, 0x21, 0x11, 0x09,
    // Right Pad: H, J, K, L, ;, Tab (->)
    0x44, 0x24, 0x14, 0x0c, 0x18, 0x80,
    // Right Pad: N, M, Comma, Period, Slash, Space
    0x50, 0x48, 0x28, 0x60, 0x30, 0x0c
};

static uint8_t jcv_crvision_input_rd(int keylatch) {
    /* There is simply no getting around this function being ugly while still
       being somewhat understandable to anyone who wishes to modify it in the
       future. Whoever designed the CreatiVision input system did the world
       a disservice.
    */
    int port = keylatch > 2 ? 1 : 0;
    unsigned pstate = jcv_crvision_input_cb(udata_input, port);
    uint8_t bits = 0xff; // Active Low

    unsigned u = (pstate & CRV_INPUT_UP);
    unsigned d = (pstate & CRV_INPUT_DOWN);
    unsigned l = (pstate & CRV_INPUT_LEFT);
    unsigned r = (pstate & CRV_INPUT_RIGHT);

    switch (keylatch) {
        case 0x01: { // PA0 - Left Joystick
            // Check for diagonal combinations
            if (d && r)
                bits &= ~0x03;
            else if (u && r)
                bits &= ~0x44;
            else if (u && l)
                bits &= ~0x30;
            else if (d && l)
                bits &= ~0x42;
            else {
                if (u) bits &= ~crv_input_map[0];
                if (d) bits &= ~crv_input_map[1];
                if (l) bits &= ~crv_input_map[2];
                if (r) bits &= ~crv_input_map[3];
            }

            if (pstate & CRV_INPUT_FIRE2) bits &= ~crv_input_map[5];
            if (pstate & CRV_INPUT_1) bits &= ~crv_input_map[6];
            if (pstate & CRV_INPUT_CNTL) bits &= ~crv_input_map[12];
            break;
        }
        case 0x02: { // PA1 - Left Keyboard
            if (pstate & CRV_INPUT_FIRE1) bits &= ~crv_input_map[4];

            if (pstate & CRV_INPUT_2) bits &= ~crv_input_map[7];
            if (pstate & CRV_INPUT_3) bits &= ~crv_input_map[8];
            if (pstate & CRV_INPUT_4) bits &= ~crv_input_map[9];
            if (pstate & CRV_INPUT_5) bits &= ~crv_input_map[10];
            if (pstate & CRV_INPUT_6) bits &= ~crv_input_map[11];
            if (pstate & CRV_INPUT_Q) bits &= ~crv_input_map[13];
            if (pstate & CRV_INPUT_W) bits &= ~crv_input_map[14];
            if (pstate & CRV_INPUT_E) bits &= ~crv_input_map[15];
            if (pstate & CRV_INPUT_R) bits &= ~crv_input_map[16];
            if (pstate & CRV_INPUT_T) bits &= ~crv_input_map[17];
            if (pstate & CRV_INPUT_BACKSPACE) bits &= ~crv_input_map[18];
            if (pstate & CRV_INPUT_A) bits &= ~crv_input_map[19];
            if (pstate & CRV_INPUT_S) bits &= ~crv_input_map[20];
            if (pstate & CRV_INPUT_D) bits &= ~crv_input_map[21];
            if (pstate & CRV_INPUT_F) bits &= ~crv_input_map[22];
            if (pstate & CRV_INPUT_G) bits &= ~crv_input_map[23];
            if (pstate & CRV_INPUT_SHIFT) bits &= ~crv_input_map[24];
            if (pstate & CRV_INPUT_Z) bits &= ~crv_input_map[25];
            if (pstate & CRV_INPUT_X) bits &= ~crv_input_map[26];
            if (pstate & CRV_INPUT_C) bits &= ~crv_input_map[27];
            if (pstate & CRV_INPUT_V) bits &= ~crv_input_map[28];
            if (pstate & CRV_INPUT_B) bits &= ~crv_input_map[29];
            break;
        }
        case 0x04: { // PA2 - Right Joystick
            // Check for diagonal combinations
            if (d && r)
                bits &= ~0x03;
            else if (u && r)
                bits &= ~0x44;
            else if (u && l)
                bits &= ~0x30;
            else if (d && l)
                bits &= ~0x42;
            else {
                if (u) bits &= ~crv_input_map[0];
                if (d) bits &= ~crv_input_map[1];
                if (l) bits &= ~crv_input_map[2];
                if (r) bits &= ~crv_input_map[3];
            }

            if (pstate & CRV_INPUT_FIRE2) bits &= ~crv_input_map[5];
            if (pstate & CRV_INPUT_TAB) bits &= ~crv_input_map[47];
            if (pstate & CRV_INPUT_SPACE) bits &= ~crv_input_map[53];
            break;
        }
        case 0x08: { // PA3 - Right Keyboard
            if (pstate & CRV_INPUT_FIRE1) bits &= ~crv_input_map[4];

            if (pstate & CRV_INPUT_7) bits &= ~crv_input_map[30];
            if (pstate & CRV_INPUT_8) bits &= ~crv_input_map[31];
            if (pstate & CRV_INPUT_9) bits &= ~crv_input_map[32];
            if (pstate & CRV_INPUT_0) bits &= ~crv_input_map[33];
            if (pstate & CRV_INPUT_COLON) bits &= ~crv_input_map[34];
            if (pstate & CRV_INPUT_MINUS) bits &= ~crv_input_map[35];
            if (pstate & CRV_INPUT_Y) bits &= ~crv_input_map[36];
            if (pstate & CRV_INPUT_U) bits &= ~crv_input_map[37];
            if (pstate & CRV_INPUT_I) bits &= ~crv_input_map[38];
            if (pstate & CRV_INPUT_O) bits &= ~crv_input_map[39];
            if (pstate & CRV_INPUT_P) bits &= ~crv_input_map[40];
            if (pstate & CRV_INPUT_RETN) bits &= ~crv_input_map[41];
            if (pstate & CRV_INPUT_H) bits &= ~crv_input_map[42];
            if (pstate & CRV_INPUT_J) bits &= ~crv_input_map[43];
            if (pstate & CRV_INPUT_K) bits &= ~crv_input_map[44];
            if (pstate & CRV_INPUT_L) bits &= ~crv_input_map[45];
            if (pstate & CRV_INPUT_SEMICOLON) bits &= ~crv_input_map[46];
            if (pstate & CRV_INPUT_N) bits &= ~crv_input_map[48];
            if (pstate & CRV_INPUT_M) bits &= ~crv_input_map[49];
            if (pstate & CRV_INPUT_COMMA) bits &= ~crv_input_map[50];
            if (pstate & CRV_INPUT_PERIOD) bits &= ~crv_input_map[51];
            if (pstate & CRV_INPUT_SLASH) bits &= ~crv_input_map[52];
            break;
        }
        case 0x0f: {
            break;
        }
        default: {
            // Multiple PA lines low - special scanning mode
            break;
        }
    }
    return bits;
}

void jcv_crvision_input_set_callback(unsigned (*cb)(const void*,int), void *u) {
    jcv_crvision_input_cb = cb;
    udata_input = u;
}

void* jcv_crvision_get_ram_data(void) {
    return &ram[0];
}

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

// Load raw state data into the running system
void jcv_crvision_state_load_raw(const void *sstate) {
    uint8_t *st = (uint8_t*)sstate;
    jcv_serial_begin();
    uint32_t ver = jcv_serial_pop32(st);
    (void)ver;
    jcv_serial_popblk(ram, st, SIZE_1K);
    pia.ddr[0] = jcv_serial_pop8(st);
    pia.ddr[1] = jcv_serial_pop8(st);
    pia.or[0] = jcv_serial_pop8(st);
    pia.or[1] = jcv_serial_pop8(st);
    pia.cr[0] = jcv_serial_pop8(st);
    pia.cr[1] = jcv_serial_pop8(st);
    sn76489_state_load(&psg, st);
    tms9918_state_load(st);
    jcv_m6502_state_load(st);
}

// Snapshot the running state and return the address of the raw data
const void* jcv_crvision_state_save_raw(void) {
    jcv_serial_begin();
    jcv_serial_push32(state, state_version);
    jcv_serial_pushblk(state, ram, SIZE_1K);
    jcv_serial_push8(state, pia.ddr[0]);
    jcv_serial_push8(state, pia.ddr[1]);
    jcv_serial_push8(state, pia.or[0]);
    jcv_serial_push8(state, pia.or[1]);
    jcv_serial_push8(state, pia.cr[0]);
    jcv_serial_push8(state, pia.cr[1]);
    sn76489_state_save(&psg, state);
    tms9918_state_save(state);
    jcv_m6502_state_save(state);
    return (const void*)state;
}

static uint8_t jcv_crvision_rom_null_rd40(uint16_t addr) {
    (void)addr;
    return 0xff;
}

static uint8_t jcv_crvision_rom_4k_rd80(uint16_t addr) {
    return romdata[addr & 0xfff];
}

static uint8_t jcv_crvision_rom_8k_rd80(uint16_t addr) {
    return romdata[addr & 0x1fff];
}

static uint8_t jcv_crvision_rom_12k_rd40(uint16_t addr) {
    return romdata[SIZE_8K + (addr & 0xfff)];
}

static uint8_t jcv_crvision_rom_12k_rd80(uint16_t addr) {
    return romdata[addr & 0x1fff];
}

static uint8_t jcv_crvision_rom_18k_rd80(uint16_t addr) {
    if (addr >= 0x8000 && addr < 0xa000)
        return rombank2_8k[addr & 0x1fff]; // Second 8K bank
    else if (addr >= 0xa000 && addr < 0xc000)
        return rombank1_8k[addr & 0x1fff]; // First 8K bank
    return 0xff;
}

static uint8_t jcv_crvision_rom_18k_rd40(uint16_t addr) {
    return rombank_2k[addr & 0x7ff];
}

// Load the CreatiVision BIOS from a memory buffer
int jcv_crvision_bios_load(void *data, size_t size) {
    if (size != SIZE_CRVBIOS)
        return 0;
    biosdata = (uint8_t*)calloc(size, sizeof(uint8_t));
    memcpy(biosdata, data, size);
    return 1;
}

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

    jcv_log(JCV_LOG_DBG, "ROM size: %ld, 0x%04lx\n", romsize, romsize);
    switch (romsize) {
        case SIZE_4K: {
            jcv_log(JCV_LOG_DBG, "Configured as 4K ROM\n");
            jcv_crvision_rom_rd40 = &jcv_crvision_rom_null_rd40;
            jcv_crvision_rom_rd80 = &jcv_crvision_rom_4k_rd80;
            break;
        }
        case SIZE_8K: {
            jcv_log(JCV_LOG_DBG, "Configured as 8K ROM\n");
            jcv_crvision_rom_rd40 = &jcv_crvision_rom_null_rd40;
            jcv_crvision_rom_rd80 = &jcv_crvision_rom_8k_rd80;
            break;
        }
        case SIZE_12K: {
            jcv_log(JCV_LOG_DBG, "Configured as 12K ROM\n");
            jcv_crvision_rom_rd40 = &jcv_crvision_rom_12k_rd40;
            jcv_crvision_rom_rd80 = &jcv_crvision_rom_12k_rd80;
            break;
        }
        case SIZE_18K: {
            jcv_log(JCV_LOG_DBG, "Configured as 18K ROM\n");
            /* Only one game is 18K, Chopper Rescue. Detect how the ROM chips
               are concatenated to form the file. There are three known
               configurations.
            */
            uint8_t *rom = (uint8_t*)data;
            uint32_t sig0000 = (rom[0] << 24) | (rom[1] << 16) |
                (rom[2] << 8) | rom[3];
            uint32_t sig2000 = (rom[0x2000] << 24) | (rom[0x2001] << 16) |
                (rom[0x2002] << 8) | rom[0x2003];
            // Check for a5 62 18 69 to determine the ROM configuration
            if (sig0000 == 0xa5621869) {
                // Standard format (2/1/3)
                rombank1_8k = romdata + 0x2000;
                rombank2_8k = romdata + 0x0000;
                rombank_2k = romdata + 0x4000;
            }
            else if (sig2000 == 0xa5621869) {
                // Manual concatenated format (1/2/3)
                rombank1_8k = romdata + 0x0000;
                rombank2_8k = romdata + 0x2000;
                rombank_2k = romdata + 0x4000;
            }
            else { // Alt 1 format (1/3/2)
                rombank1_8k = romdata + 0x0000;
                rombank2_8k = romdata + 0x2800;
                rombank_2k = romdata + 0x2000;
            }

            jcv_crvision_rom_rd40 = &jcv_crvision_rom_18k_rd40;
            jcv_crvision_rom_rd80 = &jcv_crvision_rom_18k_rd80;
            break;
        }
        default: {
            jcv_log(JCV_LOG_ERR, "Unsupported ROM size: %ld bytes\n", romsize);
            return 0;
        }
    }

    return 1;
}

/* CreatiVision Read Map
   0x0000 - 0x0fff: RAM (Mirrored every 1K)
   0x1000 - 0x1fff: PIA (Mirrored every 4 bytes)
   0x2000 - 0x2fff: VDP (Mirrored every other byte)
   0x4000 - 0x7fff: 8K ROM Bank 2
   0x8000 - 0xbfff: 8K ROM Bank 1
   0xe801         : Centronics Status
*/
uint8_t jcv_crvision_mem_rd(uint16_t addr) {
    if (addr < 0x1000) { // RAM (mirrored)
        return ram[addr & 0x3ff];
    }
    else if (addr < 0x2000) { // PIA
        if ((addr & 0x03) == 0x02) { // Port B read
            if ((pia.cr[1] & 0x04) == 0) {
                // DDR selected, return DDR value
                return pia.ddr[1];
            }
            else {
                // Output register selected
                if (pia.ddr[1] == 0xff) {
                    // When all bits are output, return the output register
                    return pia.or[1];
                }
                else {
                    int keylatch = (~pia.or[0]) & 0x0f;
                    return jcv_crvision_input_rd(keylatch);
                }
            }
        }

        return 0xff; // Other PIA registers
    }
    else if (addr < 0x3000) { // VDP
        return addr & 1 ? tms9918_rd_stat() : tms9918_rd_data();
    }
    else if (addr < 0x4000) { // Empty
        return 0xff;
    }
    else if (addr < 0x8000) { // ROM bank 2
        return jcv_crvision_rom_rd40(addr);
    }
    else if (addr < 0xc000) { // ROM bank 1
        return jcv_crvision_rom_rd80(addr);
    }
    else if (addr == 0xe801) { // Centronics Status
        jcv_log(JCV_LOG_DBG, "Centronics Status read\n");
        return 0xff;
    }
    else if (addr >= 0xf800) { // BIOS ROM
        return biosdata[addr & 0x7ff];
    }

    jcv_log(JCV_LOG_DBG, "rd: %04x\n", addr);
    return 0xff;
}

/* CreatiVision Write Map
   0x0000 - 0x0fff: RAM (Mirrored every 1K)
   0x1000 - 0x1fff: PIA (Mirrored every 4 bytes)
   0x3000 - 0x3fff: VDP (Mirrored every other byte)
   0xe800         : Centronics Data
   0xe801         : Centronics Control
*/
void jcv_crvision_mem_wr(uint16_t addr, uint8_t data) {
    if (addr < 0x1000) { // RAM
        ram[addr & 0x3ff] = data;
        return;
    }

    switch (addr & 0xf000) {
        case 0x1000: {
            switch (addr & 0x03) {
                case 0:
                    if ((pia.cr[0] & 0x04) == 0) {
                        pia.ddr[0] = data;
                    }
                    else {
                        pia.or[0] = data;
                    }
                    return;
                case 1:
                    pia.cr[0] = data;
                    return;
                case 2:
                    if ((pia.cr[1] & 0x04) == 0) {
                        pia.ddr[1] = data;
                    }
                    else {
                        pia.or[1] = data;
                        sn76489_wr(&psg, data);
                    }
                    return;
                case 3:
                    pia.cr[1] = data;
                    return;
            }
            break;
        }
        case 0x3000: { // VDP
            addr & 1 ? tms9918_wr_ctrl(data) : tms9918_wr_data(data);
            return;
        }
        case 0xe000: {
            jcv_log(JCV_LOG_DBG, "Centronics Write\n");
            return;
        }
    }

    jcv_log(JCV_LOG_DBG, "wr: %04x, %02x\n", addr, data);
}

void jcv_crvision_init(void) {
    // Initialize sound chip
    sn76489_init(&psg);
    jcv_mixer_set_sn76489(&psg);

    // Clear RAM
    for (unsigned i = 0; i < sizeof(ram); ++i)
        ram[i] = 0x00;

    // Initialize PIA registers
    pia.or[0] = pia.ddr[0] = pia.cr[0] = 0;
    pia.or[1] = pia.ddr[1] = pia.cr[1] = 0;
}

// Deinitialize any allocated memory
void jcv_crvision_deinit(void) {
    if (biosdata)
        free(biosdata);
}

/*
 * 2 000 000 Hz / 50Hz = 40,000 CPU cycles per frame
 * 2 000 000 Hz / 50Hz / 313 lines = 127.79553 CPU cycles per line
*/
void jcv_crvision_exec(void) {
    // Restore the leftover cycle count
    uint32_t extcycs = jcv_m6502_cyc_restore();

    // Run scanline-based iterations of emulation until a frame is complete
    for (size_t i = 0; i < NUM_SCANLINES; ++i) {
        // Set the number of cycles required to complete this scanline
        size_t reqcycs = M6502_CYC_LINE - extcycs;

        // Count cycles for an iteration (usually one instruction)
        size_t itercycs = 0;

        // Count the total cycles run in a scanline
        size_t linecycs = 0;

        // Run CPU instructions until enough have been run for one scanline
        while (linecycs < reqcycs) {
            itercycs = jcv_m6502_exec(); // Run a single CPU instruction
            linecycs += itercycs; // Add the number of cycles to the total

            tms9918_intchk(); // Keep IRQ line asserted if necessary

            for (size_t s = 0; s < itercycs; ++s) { // Catch PSG up to the CPU
                if (++psgcycs % DIV_PSG == 0) {
                    sn76489_exec(&psg);
                    psgcycs = 0;
                }
            }
        }

        extcycs = linecycs - reqcycs; // Store extra cycle count

        tms9918_exec(); // Draw a scanline of pixel data
    }

    // Resample audio and push to the frontend
    jcv_mixer_resamp();

    // Store the leftover cycle count
    jcv_m6502_cyc_store(extcycs);
}
