/*
 * Copyright (c) 2021-2025 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_sms_io.h"
#include "smsvdp.h"

#define NUMIOPORTS 2

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

// Flip-flops for SMS Paddles
static unsigned flip[NUMIOPORTS] = { 0, 0 };

/* Control Ports
   =========================================
   | Bit | Port 0xdc      | Port 0xdd      |   Z80 reads to these port
   =========================================   addresses do not correspond
   |  7  | Port B Down    | Port B TH      |   directly to physical ports.
   |  6  | Port B Up      | Port A TH      |
   |  5  | Port A TR      | Cartridge CONT |   Cartridge CONT is largely
   |  4  | Port A TL      | Reset          |   irrelevant.
   |  3  | Port A Right   | Port B TR      |
   |  2  | Port A Left    | Port B TL      |   Some systems do not have a Reset
   |  1  | Port A Down    | Port B Right   |   button, and in these cases a 1
   |  0  | Port A Up      | Port B Left    |   is always returned.
   -----------------------------------------

   TL is Button 1, TR is Button 2. TH is used for both region detection and the
   light phaser's light sensor.
*/

// 2 Button Control Pad
static uint8_t cega_sms_io_rd_pad(int port) {
    uint8_t bits = 0x00;

    /* All return values are complemented. This means any bits set in this
       function will be returned as 0s, and unset bits will be returned as 1s.
    */
    if (port) { // Port 0xdd
        /* System buttons (Reset and Pause) are implemented as a separate
           emulated device, so two polls are done to gather all required bits.
        */
        uint8_t padstate = cega_input_cb[1](1); // Pad 2 State
        uint8_t sysstate = cega_input_cb[2](2); // System State

        if (padstate & SMS_INPUT_L) bits |= (1 << 0);
        if (padstate & SMS_INPUT_R) bits |= (1 << 1);
        if (padstate & SMS_INPUT_1) bits |= (1 << 2);
        if (padstate & SMS_INPUT_2) bits |= (1 << 3);
        if (sysstate & SMS_INPUT_RESET) bits |= (1 << 4);

        // If there is Light Phaser active and triggered, set the TH bit
        if (smsvdp_phaser_triggered()) bits |= (1 << 6);
    }
    else { // Port 0xdc
        /* Since two physical ports need to be accessed by this read, two
           physical ports are polled to gather all required bits.
        */
        uint8_t padstate1 = cega_input_cb[0](0); // Pad 1 State
        uint8_t padstate2 = cega_input_cb[1](1); // Pad 2 State

        if (padstate1 & SMS_INPUT_U) bits |= (1 << 0);
        if (padstate1 & SMS_INPUT_D) bits |= (1 << 1);
        if (padstate1 & SMS_INPUT_L) bits |= (1 << 2);
        if (padstate1 & SMS_INPUT_R) bits |= (1 << 3);
        if (padstate1 & SMS_INPUT_1) bits |= (1 << 4);
        if (padstate1 & SMS_INPUT_2) bits |= (1 << 5);
        if (padstate2 & SMS_INPUT_U) bits |= (1 << 6);
        if (padstate2 & SMS_INPUT_D) bits |= (1 << 7);
    }

    return ~bits;
}

// SMS Paddle Control
static uint8_t cega_sms_io_rd_paddle(int port) {
    /* Paddle Control Bits
       ======================================
       | TR | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |  The paddle position is an 8-bit
       ======================================  value which is broken up into
       |  1 | R | L | D | U |   |   |   |   |  two reads, with the four
       --------------------------------------  directional pins on each read
       |  0 |   |   |   |   | R | L | D | U |  making up the final 8-bit value.
       --------------------------------------

       The TR value is flipped each time the paddle is polled. In reality this
       is done using a clock crystal, but flipping the bits manually each read
       has the same effect. One caveat is that normally, bits that are set
       should be returned low (0) but in the case of position they should be
       set high (1). The dial rotates in a fixed range (0-255) and is not
       continuous.

       Paddles have two buttons, both corresponding to Button 1 (TL).
       In this implementation, buttons are polled separately from position.
    */
    uint8_t bits = 0x00;

    if (port) { // Port 0xdd
        uint8_t pos2 = cega_input_cb[port](port); // Player 2 Position

        if (cega_input_cb[port](port + NUMIOPORTS)) // Player 2 TL
            bits |= (1 << 2);

        if (flip[1]) { // Player 2 Left/Right
            bits |= ~(pos2 >> 6) & 0x03;
        }
        else {
            bits |= ~(pos2 >> 2) & 0x03;
            bits |= 0x08; // Player 2 TR
        }
    }
    else { // Port 0xdc
        // As usual, two physical ports must be read to gather all bits.
        uint8_t pos1 = cega_input_cb[0](0); // Player 1 Position
        uint8_t pos2 = cega_input_cb[1](1); // Player 2 Position

        if (cega_input_cb[port](port + NUMIOPORTS)) // Player 1 TL
            bits |= (1 << 4);

        if (flip[0]) {
            bits |= ~(pos1 >> 4) & 0x0f;
        }
        else {
            bits |= ~pos1 & 0x0f;
            bits |= 0x20; // Player 1 TR
        }

        if (flip[1]) { // Player 2 Up/Down
            bits |= ~(pos2 << 2) & 0xc0;
        }
        else {
            bits |= ~(pos2 << 6) & 0xc0;
        }
    }

    flip[port] ^= 1;

    return ~bits;
}

// Light Phaser
static uint8_t cega_sms_io_rd_phaser(int port) {
    if (port) { } // Light Phasers are only supported in Port A (Player 1)

    /* Two polls are done to gather bits from the light phaser and the control
       pad, as the Up and Down pins for Port B are returned in reads to 0xdc.
       Some games (Laser Ghost, Operation Wolf) use the control pad plugged
       into Port B for some aspects of gameplay. Laser Ghost even requires the
       control pad to be used to start the game in order for the Light Phaser
       to work.
    */
    uint8_t phstate = cega_input_cb[0](0);
    uint8_t padstate = cega_input_cb[1](1); // Pad 2 State - TL only (trigger)

    /* Only TL and any Port B direction bits are set here. TL represents the
       trigger, while TH represents the light sensor. Setting TH is implemented
       in other input polling callbacks as TH for both Port A/B are read on
       Port 0xdd, and the implementation of scanning the light sensor for
       matches is done in the SMS VDP.
    */
    uint8_t bits = phstate | ((padstate << 6) & 0xc0);

    // Be sure to keep the phaser on stun and don't try to rob any banks
    return ~bits;
}

// Sports Pad
/*static uint8_t cega_sms_io_rd_sports(int port) {
    return 0xff;
}*/

// None (Unplugged)
static uint8_t cega_sms_io_rd_none(int port) {
    // If the light phaser is triggered, set TH low on reads to Port 0xdd.
    if (port && smsvdp_phaser_triggered())
        return 0xbf;
    else
        return 0xff;
}

// NMI callback from SMS VDP
static void cega_sms_io_vdpnmi(void) {
    smsvdp_t *vdp = smsvdp_ctx();
    if (cega_input_cb[2](2) & SMS_INPUT_PAUSE) {
        if (!vdp->paused) {
            vdp->paused = 1;
            cega_z80_nmi(0);
        }
    }
    else {
        vdp->paused = 0;
        cega_z80_nmi_clr(0);
    }
}

uint8_t cega_sms_io_rd(uint8_t port) {
    return cega_sms_io_rd_port[port](port);
}

void cega_sms_io_set_port(int port, int type) {
        switch (type) {
        default:
            cega_sms_io_rd_port[port] = &cega_sms_io_rd_none;
            break;
        case SMS_INPUT_TYPE_PAD:
            cega_sms_io_rd_port[port] = &cega_sms_io_rd_pad;
            break;
        case SMS_INPUT_TYPE_PADDLE:
            cega_sms_io_rd_port[port] = &cega_sms_io_rd_paddle;
            break;
        case SMS_INPUT_TYPE_PHASER:
            cega_sms_io_rd_port[port] = &cega_sms_io_rd_phaser;
            break;
        /*case SMS_INPUT_TYPE_SPORTS:
            cega_sms_io_rd_port[port] = &cega_sms_io_rd_sports;
            break;*/
    }
}

void cega_sms_io_init(void) {
    cega_sms_io_rd_port[0] = &cega_sms_io_rd_pad;
    cega_sms_io_rd_port[1] = &cega_sms_io_rd_pad;
    smsvdp_set_nmi_callback(&cega_sms_io_vdpnmi);
}
