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

#include "cega.h"
#include "cega_m68k.h"
#include "cega_md_io.h"

/* (896040 / 7) = 128005.71 (Master Cycles per NTSC Frame / 68K Divider)
 * 1000 / 59.92274 = 16.688155 (Milliseconds per Frame)
 * 128005.71 / 16.688155 = 7670.4531 (68K cycles per Millisecond)
 * 7670.4531 * 1.5 = 11505.68, or ~11520
 */
#define TIMEOUT_6B 11520 // Timeout in 68K cycles, approximately 1.5ms
#define NUMIOPORTS 3 // Control Port 1, Control Port 2, Expansion

// Function pointer array for IO Port reads
static uint8_t (*cega_md_io_rd_port[NUMIOPORTS])(int);

static md_ioport_t ioport[NUMIOPORTS];

static uint8_t cega_md_io_rd_pad6b(int port) {
    /* 6 Button Control Pad
       ================================================
       | Pulse |  TH  | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
       ================================================  Key:
       |   0   | HIGH | 0 | 1 | C | B | R | L | D | U |   A = Button A
       ------------------------------------------------   B = Button B
       |   1   |  LOW | 0 | 0 | S | A | 0 | 0 | D | U |   C = Button C
       ------------------------------------------------   X = Button X
       |   1   | HIGH | 0 | 1 | C | B | R | L | D | U |   Y = Button Y
       ------------------------------------------------   Z = Button Z
       |   2   |  LOW | 0 | 0 | S | A | 0 | 0 | D | U |   S = Start
       ------------------------------------------------   M = Mode
       |   2   | HIGH | 0 | 1 | C | B | R | L | D | U |   U = D-Pad Up
       ------------------------------------------------   D = D-Pad Down
       |   3   |  LOW | 0 | 0 | S | A | 0 | 0 | 0 | 0 |   L = D-Pad Left
       ------------------------------------------------   R = D-Pad Right
       |   3   | HIGH | 0 | 1 | C | B | M | X | Y | Z |
       ------------------------------------------------
       |   4   |  LOW | 0 | 0 | S | A | 1 | 1 | 1 | 1 |  Bit 6's value is TH
       ------------------------------------------------
       |  4/0  | HIGH | 0 | 1 | C | B | R | L | D | U |  Pulse 4 TH High wraps
       ------------------------------------------------

       The 6 Button Control Pad is detected by strobing multiple times within
       a short time interval. If the correct bits (0, 1) are low when TH is set
       low on TH Pulse 3, it allows the software to determine that a 6 Button
       Control Pad is plugged in. When the TH bit on the corresponding IO
       port's data register is then set high, buttons X, Y, Z, and Mode are
       read in. The next time TH is low, the bottom 4 bits (0, 1, 2, 3) are
       forced high, and the process begins anew on the next strobe.
    */
    uint8_t bits = 0x80;
    uint16_t padstate = cega_md_input_cb[port](port);

    /* The step counter resets to 0 if a strobe has not happened within a
       specific time interval. This is not exact, as in hardware it relies on
       a capacitor's ability to hold a charge. An approximate timeout value is
       used in this instance.
    */
    uint64_t cyc = cega_m68k_cyc_total();
    if ((cyc - ioport[port].cyc) > TIMEOUT_6B)
        ioport[port].step = 0;
    ioport[port].cyc = cyc;

    /* All return values are complemented. This means any bits set in this
       function will be read as 0s. This is why the TH bit is set when the TH
       bit is low -- it will be inverted when it is returned. Buttons are also
       considered pressed when they are low, so in this function they are set
       high.
    */
    if (ioport[port].data & 0x40) { // TH High (Bit 6)
        if (ioport[port].step == 3) {
            if (padstate & MD_INPUT_Z) bits |= (1 << 0);
            if (padstate & MD_INPUT_Y) bits |= (1 << 1);
            if (padstate & MD_INPUT_X) bits |= (1 << 2);
            if (padstate & MD_INPUT_M) bits |= (1 << 3);
            if (padstate & MD_INPUT_B) bits |= (1 << 4);
            if (padstate & MD_INPUT_C) bits |= (1 << 5);
        }
        else {
            if (padstate & MD_INPUT_U) bits |= (1 << 0);
            if (padstate & MD_INPUT_D) bits |= (1 << 1);
            if (padstate & MD_INPUT_L) bits |= (1 << 2);
            if (padstate & MD_INPUT_R) bits |= (1 << 3);
            if (padstate & MD_INPUT_B) bits |= (1 << 4);
            if (padstate & MD_INPUT_C) bits |= (1 << 5);
        }
        if (++ioport[port].step == 5)
            ioport[port].step = 1;
        return ~bits;
    }
    else {
        if (ioport[port].step == 3) {
            bits |= 0x4f;
            if (padstate & MD_INPUT_A) bits |= (1 << 4);
            if (padstate & MD_INPUT_S) bits |= (1 << 5);
        }
        else if (ioport[port].step == 4) {
            bits |= 0x40;
            if (padstate & MD_INPUT_A) bits |= (1 << 4);
            if (padstate & MD_INPUT_S) bits |= (1 << 5);
        }
        else {
            bits |= 0x4c;
            if (padstate & MD_INPUT_U) bits |= (1 << 0);
            if (padstate & MD_INPUT_D) bits |= (1 << 1);
            if (padstate & MD_INPUT_A) bits |= (1 << 4);
            if (padstate & MD_INPUT_S) bits |= (1 << 5);
        }
        return ~bits;
    }
}

static uint8_t cega_md_io_rd_pad3b(int port) {
    /* 3 Button Control Pad
       ========================================
       |  TH  | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
       ======================================== Key:
       | HIGH | 0 | 1 | C | B | R | L | D | U |  A = Button A   B = Button B
       ----------------------------------------  C = Button C   S = Start
       |  LOW | 0 | 0 | S | A | 0 | 0 | D | U |  U = D-Pad Up   D = D-Pad Down
       ----------------------------------------  L = D-Pad Left R = D-Pad Right

       Two strobes are necessary to read the 3 Button Control Pad. When TH is
       high, the Directions, Buttons B, and Button C are read. When TH is low,
       Button A, Start, and Up/Down are read. Bit 6 is the TH value.
    */
    uint8_t bits = 0x80;
    uint16_t padstate = cega_md_input_cb[port](port);

    // As with 6 Button Control Pad reads, all return values are complemented.
    if (ioport[port].data & 0x40) { // TH set (Bit 6)
        if (padstate & MD_INPUT_U) bits |= (1 << 0);
        if (padstate & MD_INPUT_D) bits |= (1 << 1);
        if (padstate & MD_INPUT_L) bits |= (1 << 2);
        if (padstate & MD_INPUT_R) bits |= (1 << 3);
        if (padstate & MD_INPUT_B) bits |= (1 << 4);
        if (padstate & MD_INPUT_C) bits |= (1 << 5);
    }
    else {
        bits |= 0x4c;
        if (padstate & MD_INPUT_U) bits |= (1 << 0);
        if (padstate & MD_INPUT_D) bits |= (1 << 1);
        if (padstate & MD_INPUT_A) bits |= (1 << 4);
        if (padstate & MD_INPUT_S) bits |= (1 << 5);
    }
    return ~bits;
}

// Return all 1s when nothing is plugged in
static uint8_t cega_md_io_rd_none(int port) {
    if (port) { }
    return 0xff;
}

// Read from an IO Port
uint8_t cega_md_io_rd(uint32_t addr) {
    switch (addr | 1) {
        case 0x03: // REG_DATA1
            return cega_md_io_rd_port[0](0);
        case 0x05: // REG_DATA2
            return cega_md_io_rd_port[1](1);
        case 0x07: // REG_DATA3
            return cega_md_io_rd_port[2](2);
        case 0x09: // REG_CTRL1
            return ioport[0].ctrl;
        case 0x0b: // REG_CTRL2
            return ioport[1].ctrl;
        case 0x0d: // REG_CTRL3
            return ioport[2].ctrl;
        case 0x0f: // REG_TXDATA1
            return ioport[0].txdata;
        case 0x11: // REG_RXDATA1
            return ioport[0].rxdata;
        case 0x13: // REG_S_CTRL1
            return ioport[0].sctrl;
        case 0x15: // REG_TXDATA2
            return ioport[1].txdata;
        case 0x17: // REG_RXDATA2
            return ioport[1].rxdata;
        case 0x19: // REG_S_CTRL2
            return ioport[1].sctrl;
        case 0x1b: // REG_TXDATA3
            return ioport[2].txdata;
        case 0x1d: // REG_RXDATA3
            return ioport[2].rxdata;
        case 0x1f: // REG_S_CTRL3
            return ioport[2].sctrl;
        default: {
            cega_log(CEGA_LOG_DBG, "md_io_rd: %02x\n", addr);
            return 0xff;
        }
    }
}

// Write to an IO Port
void cega_md_io_wr(uint32_t addr, uint8_t data) {
    switch (addr | 1) {
        case 0x03: // REG_DATA1
            ioport[0].data = data;
            break;
        case 0x05: // REG_DATA2
            ioport[1].data = data;
            break;
        case 0x07: // REG_DATA3
            ioport[2].data = data;
            break;
        case 0x09: // REG_CTRL1
            ioport[0].ctrl = data;
            break;
        case 0x0b: // REG_CTRL2
            ioport[1].ctrl = data;
            break;
        case 0x0d: // REG_CTRL3
            ioport[2].ctrl = data;
            break;
        case 0x0f: // REG_TXDATA1
            ioport[0].txdata = data;
            break;
        case 0x11: // REG_RXDATA1
            ioport[0].rxdata = data;
            break;
        case 0x13: // REG_S_CTRL1
            ioport[0].sctrl = data;
            break;
        case 0x15: // REG_TXDATA2
            ioport[1].txdata = data;
            break;
        case 0x17: // REG_RXDATA2
            ioport[1].rxdata = data;
            break;
        case 0x19: // REG_S_CTRL2
            ioport[1].sctrl = data;
            break;
        case 0x1b: // REG_TXDATA3
            ioport[2].txdata = data;
            break;
        case 0x1d: // REG_RXDATA3
            ioport[2].rxdata = data;
            break;
        case 0x1f: // REG_S_CTRL3
            ioport[2].sctrl = data;
            break;
        default: {
            cega_log(CEGA_LOG_DBG, "md_io_wr: %02x %02x\n", addr, data);
            break;
        }
    }
}

// Set the device type attached to an IO Port
void cega_md_io_set_port(int port, int type) {
    switch (type) {
        default:
            cega_md_io_rd_port[port] = &cega_md_io_rd_none;
            break;
        case MD_INPUT_TYPE_PAD3B:
            cega_md_io_rd_port[port] = &cega_md_io_rd_pad3b;
            break;
        case MD_INPUT_TYPE_PAD6B:
            cega_md_io_rd_port[port] = &cega_md_io_rd_pad6b;
            break;
    }
}

// Initialize the IO Ports
void cega_md_io_init(void) {
    // Default to 6 Button Control Pad in both ports, nothing in expansion port
    cega_md_io_rd_port[0] = &cega_md_io_rd_pad6b;
    cega_md_io_rd_port[1] = &cega_md_io_rd_pad6b;
    cega_md_io_rd_port[2] = &cega_md_io_rd_none;

    // Initialize default values
    for (int i = 0; i < NUMIOPORTS; ++i) {
        ioport[i].data = 0x7f;
        ioport[i].ctrl = 0x00;
        ioport[i].txdata = 0xff;
        ioport[i].rxdata = 0x00;
        ioport[i].sctrl = 0x00;
        ioport[i].cyc = 0;
    }
}
