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

// VDP 315-5313

#include <stdint.h>
#include <stddef.h>
#include <stdio.h>

#include "m68k.h"

#include "cega.h"
#include "cega_md.h"
#include "cega_m68k.h"
#include "cega_z80.h"
#include "mdvdp.h"

// Colour Ramp pointer
static uint8_t *colramp;

/* Umbrage Colour Ramp
    This colour ramp is for anyone who takes umbrage at the Linear Colour Ramp.
    It was taken from values measured using an oscilloscope and posted here:
      http://gendev.spritesmind.net/forum/viewtopic.php?f=22&t=2188
    The same information is here: https://plutiedev.com/vdp-color-ramp
*/
static uint8_t colramp_umbrage[15] = {
    0, 29, 52, 70, 87, 101, 116, 130, 144, 158, 172, 187, 206, 228, 255
};

/* Linear Colour Ramp
    This is simply a linear ramp, with each step being 18.214286, and the final
    values being rounded to the nearest integer.
*/
static uint8_t colramp_linear[15] = {
    0, 18, 36, 55, 73, 91, 109, 128, 146, 164, 182, 200, 219, 237, 255
};

/* Dynamic Palette (Normal, Shadow, and Highlight)
    This palette is for 32-bit values output to the video buffer, converted
    from internal values. The palette is actually 0x40 large, but since we keep
    entries for Shadow and Highlight as well, it is 0xc0 (0x40 * 3). Therefore,
    three entries need to be updated every time a value is written to CRAM.
*/
//static uint32_t palette[0xc0];

static int numscanlines = MD_VDP_SCANLINES;
static int numrenderlines = MD_VDP_HEIGHT;

static uint32_t *vbuf = NULL;

static mdvdp_t vdp;

// Set the video output buffer to be written to
void mdvdp_set_buffer(uint32_t *ptr) {
    vbuf = ptr;
}

// Set the Colour Ramp
void mdvdp_set_colramp(int ramp) {
    if (ramp) // Linear
        colramp = colramp_linear;
    else // Umbrage (Hardware Accurate)
        colramp = colramp_umbrage;
}

// Set the region
void mdvdp_set_region(uint8_t region) {
    // 313 scanlines for PAL, 262 scanlines for NTSC
    numscanlines = region > REGION_US ? MD_VDP_SCANLINES_PAL : MD_VDP_SCANLINES;
}

// Callback to set video dimensions in the frontend
static void (*mdvdp_set_dimensions)(int, int, int, int);

// Set the callback that handles video dimension changes in the frontend
void mdvdp_set_size_callback(void (*cb)(int, int, int, int)) {
    mdvdp_set_dimensions = cb;
}

/*static inline void mdvdp_wr_palette(uint8_t addr) {
    // Convert 4-bit values to 8-bit values
    uint8_t r = vdp.cram[addr] & 0x000e;
    uint8_t g = (vdp.cram[addr] & 0x00e0) >> 4;
    uint8_t b = (vdp.cram[addr] & 0x0e00) >> 8;

    // Create the Normal Palette entry
    palette[addr] =
        0xff000000 | (colramp[r] << 16) | (colramp[g] << 8) | colramp[b];

    // Create the Shadow Palette entry
    r >>= 1;
    g >>= 1;
    b >>= 1;
    palette[addr + 0x40] =
        0xff000000 | (colramp[r] << 16) | (colramp[g] << 8) | colramp[b];

    // Create the Highlight Palette entry
    r += 7;
    g += 7;
    b += 7;
    palette[addr + 0x80] =
        0xff000000 | (colramp[r] << 16) | (colramp[g] << 8) | colramp[b];
}

// Draw a single pixel onto the canvas
static inline void mdvdp_pixel(uint32_t c, int line, int dot) {
    if (dot < MD_VDP_WIDTH)
        vbuf[(line * MD_VDP_WIDTH) + dot] = c;
}

static inline uint32_t mdvdp_bdcol(void) {
    return vdp.ctrl[7] & 0x3f;
}

// Check Video Mode: H40 or H32
static inline uint8_t mdvdp_h40(void) {
    return (vdp.ctrl[12] & 0x81) == 0x81;
}

// Check if Shadow/Highlight mode is enabled
static inline uint8_t mdvdp_shhl(void) {
    return vdp.ctrl[12] & 0x08;
}*/

// Check Interlace mode
static inline uint8_t mdvdp_ilc(void) {
    return vdp.ctrl[12] & 0x06;
}

// Increment address with wrap
/*static inline void mdvdp_addr_inc(void) {
    vdp.addr = (vdp.addr + vdp.ctrl[15]) & 0xffff;
}*/

static uint16_t mdvdp_rd_hcount(void) {
    return 0x1ff; // 9 bits, usually gets shifted right once
}

// Read the current VCount value
static uint8_t mdvdp_rd_vcount(void) {
    if (numscanlines == MD_VDP_SCANLINES) { // NTSC, 262 scanlines
        if (numrenderlines == MD_VDP_HEIGHT_EXT) {
            // 240 line mode: 00-ff, 00-06 (Does not work on real NTSC machines)
            if (vdp.line > 0xff)
                return vdp.line & 0xff;
            else
                return vdp.line;
        }
        else {
            // 224 line mode: 00-ea, e5-ff
            if (vdp.line > 0xea)
                return vdp.line - 6;
            else
                return vdp.line;
        }
    }
    else { // PAL, 313 scanlines
        if (numrenderlines == MD_VDP_HEIGHT_EXT) {
            // 240 line mode: 00-ff, 00-0a, d2-ff
            if (vdp.line > 0x10a)
                return vdp.line - 57;
            else if (vdp.line > 0xff)
                return vdp.line & 0xff;
            else
                return vdp.line;
        }
        else {
            // 224 line mode: 00-ff, 00-02, ca-ff
            if (vdp.line > 0x102)
                return vdp.line - 57;
            else if (vdp.line > 0xff)
                return vdp.line & 0xff;
            else
                return vdp.line;
        }
    }
}

static uint16_t mdvdp_rd_hvcount(void) {
    return (mdvdp_rd_vcount() << 8) | (mdvdp_rd_hcount() >> 1);
}

static void mdvdp_wr_ctrl(uint16_t data) {
    // Second half of command word (ADDRESS SET 2nd)
    if (vdp.wlatch) {
        vdp.wlatch = 0; // Flip Write Latch
        vdp.code &= 0x03; // Clear code register bits 2-5
        vdp.code |= ((data >> 2) & 0x3c); // Rewrite bits 2-5
        vdp.addr &= 0x3fff; // Clear address register bits 14-15
        vdp.addr |= ((data << 14) & 0xc000); // Rewrite bits 14-15
        return;
    }

    // Register Write (REGISTER SET)
    if ((data & 0xc000) == 0x8000) {
        uint8_t rnum = (data >> 8) & 0x1f;

        if (!(vdp.ctrl[1] & 0x04) && rnum > 10)
            return;

        if (rnum < 24)
            vdp.ctrl[rnum] = data & 0xff;

        // Register writes clear all of the values written in "ADDRESS Set 1st"
        vdp.code &= 0x3c;
        vdp.addr &= 0xc000;

        return;
    }

    // First half of command word (ADDRESS SET 1st)
    vdp.wlatch = 1; // Flip Write Latch
    vdp.code &= 0x3c; // Clear code register bits 0-1
    vdp.code |= ((data >> 14) & 0x03); // Rewrite bits 0-1
    vdp.addr &= 0xc000; // Clear address register bits 0-13
    vdp.addr |= (data & 0x3fff); // Rewrite bits 0-13
}

static uint16_t mdvdp_rd_data(void) {
    return 0xffff;
}

static void mdvdp_wr_data(uint16_t data) {
    if (data) { }
}

uint8_t mdvdp_rd08(uint32_t addr) {
    // Read 16 bits - Return LSB for odd addresses, MSB for even addresses
    if (addr & 0x01)
        return mdvdp_rd16(addr & 0xfffffe) & 0xff;
    else
        return (mdvdp_rd16(addr & 0xfffffe) >> 8) & 0xff;
}

uint16_t mdvdp_rd16(uint32_t addr) {
    switch (addr & 0x1f) {
        case 0x00: case 0x02: {
            return mdvdp_rd_data();
        }
        case 0x04: case 0x06: {
            vdp.wlatch = 0;
            return vdp.stat;
        }
        case 0x08: {
            return mdvdp_rd_hvcount();
        }
        case 0x18: case 0x1c: { // Unused?
            return 0xffff;
        }
    }
    cega_log(CEGA_LOG_WRN, "mdvdp_rd16: %06x\n", addr);
    return 0xffff;
}

void mdvdp_wr08(uint32_t addr, uint8_t data) {
    mdvdp_wr16(addr & 0xfffffe, (data | data << 8));
}

void mdvdp_wr16(uint32_t addr, uint16_t data) {
    switch (addr & 0x1f) {
        case 0x00: case 0x02: {
            mdvdp_wr_data(data);
            return;
        }
        case 0x04: case 0x06: {
            mdvdp_wr_ctrl(data);
            return;
        }
        case 0x18: case 0x1c: { // Unused?
            return;
        }
    }
    cega_log(CEGA_LOG_WRN, "mdvdp_wr16: %06x %04x\n", addr & 0x1f, data);
}

void mdvdp_init(void) {
    vdp.line = 0;
    vdp.dot = 0;
    vdp.stat = 0x3600;

    /* Some games require the PAL status register bit set, instead of simply
       having the correct value in the version register and correct timing.
    */
    if (numscanlines == MD_VDP_SCANLINES_PAL)
        vdp.stat |= 1; // FIXME better way to handle this?
}

void mdvdp_reset(void) {
    mdvdp_init();
}

// Test if the Line Interrupt bit is set in control register 0
static inline uint8_t mdvdp_int_ln(void) {
    return vdp.ctrl[0] & 0x10;
}

// Process Line Interrupts
static inline void mdvdp_lineint(void) {
    if ((vdp.line < numrenderlines) || (vdp.line == numscanlines)) {
        /* Decrement the line counter - if it underflows to 0xff (255), reload
           it with the last value written to Control Register 10. This behaviour
           relies on the line counter being an unsigned 8-bit integer.
        */
        if (--vdp.lcount == 0xff) {
            vdp.lcount = vdp.ctrl[10];
            cega_m68k_irq(4);
        }
    }
    else {
        // Reset the line counter to the reload value in Control Register 10
        // on the line after the active display period and thereafter.
        vdp.lcount = vdp.ctrl[10];
    }
}

int mdvdp_exec(void) {
    if (vdp.mcyc < 646) {
        ++vdp.mcyc;
        return 0;
    }

    switch (vdp.mcyc) {
        case 647: { // H40 transition from a4 to a5
            if (mdvdp_int_ln()) // Line interrupts enabled
                mdvdp_lineint();

            if (vdp.line == (numrenderlines - 1)) { // VBLANK begin
                vdp.stat |= 0x08; // VBLANK status register bit

                if (vdp.ctrl[1] & 0x20) {
                    vdp.stat |= 0x80;

                    cega_m68k_irq(6);
                }
                cega_z80_irq(0, 0xff);
            }

            if (vdp.line == (numrenderlines))
                cega_z80_irq_clr(0);

            // Increment the line number being worked on
            if (++vdp.line == numscanlines)
                vdp.line = 0;

            // Reset Dot
            vdp.dot = 0;

            break;
        }
        case 703: { // H40 transition from b2 to b3
            vdp.stat |= 0x04; // HBLANK
            break;
        }
        case 853: { // H40 transition from 05 to 06
            vdp.stat &= ~0x04;
            break;
        }
    }

    // This is the end of the line - reset the cycle counter
    if (++vdp.mcyc == 855) {
        // Reset VDP master cycle counter
        vdp.mcyc = 0;

        // Remove VBLANK status register bit
        if (vdp.line == (numscanlines - 1))
            vdp.stat &= ~0x08;

        // When the final scanline has been drawn, start over again
        if (vdp.line == 0) {
            vdp.stat &= ~0x40;

            if (mdvdp_ilc())
                vdp.stat ^= 0x10;
            else
                vdp.stat &= ~0x10;
            return 1; // Signal that an entire frame has been drawn
        }
    }

    return 0;
}
