use crate::critical;
use crate::macros::{bitfield_bool, bitfield_enum, const_new};
use crate::task;
use gba::prelude::*;
use voladdress::{Safe, VolAddress};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[repr(transparent)]
pub struct SioControl(pub u16);

impl SioControl {
    const_new!();
    bitfield_enum!(u16; 0..=1: BaudRate, baud_rate, with_baud_rate, set_baud_rate);
    bitfield_bool!(u16; 2, flow_control, with_flow_control, set_flow_control);
    bitfield_bool!(u16; 3, parity_odd, with_parity_odd, set_parity_odd);
    bitfield_bool!(u16; 4, tx_full, with_tx_full, set_tx_full);
    bitfield_bool!(u16; 5, rx_empty, with_rx_empty, set_rx_empty);
    bitfield_bool!(u16; 6, error, with_error, set_error);
    bitfield_bool!(u16; 7, data_8bit, with_data_8bit, set_data_8bit);
    bitfield_bool!(u16; 8, fifo, with_fifo, set_fifo);
    bitfield_bool!(u16; 9, parity, with_parity, set_parity);
    bitfield_bool!(u16; 10, tx_enable, with_tx_enable, set_tx_enable);
    bitfield_bool!(u16; 11, rx_enable, with_rx_enable, set_rx_enable);
    bitfield_enum!(u16; 12..=13: SioMode, mode, with_mode, set_mode);
    bitfield_bool!(u16; 14, irq, with_irq, set_irq);
}

#[repr(u16)]
pub enum SioMode {
    Normal8Bit = 0 << 12,
    MultiPlayer = 1 << 12,
    Normal32Bit = 2 << 12,
    Uart = 3 << 12,
}

#[repr(u16)]
pub enum BaudRate {
    B9600,
    B38400,
    B57600,
    B115200,
}

pub const SIOCNT_: VolAddress<SioControl, Safe, Safe> = unsafe { VolAddress::new(0x400_0128) };

pub fn init(use_fifo: bool) {
    let mut setting = SioControl::new()
        .with_baud_rate(BaudRate::B115200)
        .with_flow_control(true)
        .with_data_8bit(true)
        .with_fifo(false)
        .with_parity_odd(false)
        .with_parity(true)
        .with_tx_enable(false)
        .with_rx_enable(false)
        .with_mode(SioMode::Uart)
        .with_irq(false);

    RCNT.write(0);
    /* GBATek tells us:
     *
     * The content of the FIFO is reset when FIFO is disabled in UART mode, thus, when entering UART mode initially set FIFO=disabled.
     * The Send/Receive enable bits must be reset before switching from UART mode into another SIO mode!
     *
     * So we only turn on the FIFO, TX enable and RX enable in the second write.
     */
    SIOCNT_.write(setting);
    setting.set_fifo(use_fifo);
    setting.set_tx_enable(true);
    setting.set_rx_enable(true);
    setting.set_irq(true);
    SIOCNT_.write(setting);
}

pub fn read_byte() -> u8 {
    if SIOCNT_.read().rx_empty() {
        critical(|| {
            if SIOCNT_.read().rx_empty() {
                task::block_on(task::BlockingOn::SerialReadable);
            }
            if SIOCNT_.read().rx_empty() {
                panic!("rx still empty??");
            }
        });
    }
    SIODATA8.read()
}

pub fn read_dword() -> u32 {
    let (a, b, c, d) = (read_byte(), read_byte(), read_byte(), read_byte());
    (a as u32) | ((b as u32) << 8) | ((c as u32) << 16) | ((d as u32) << 24)
}

pub fn write_byte(data: u8) {
    if SIOCNT_.read().tx_full() {
        critical(|| {
            // Inbetween the previous read and the start of the critical section, tx_full may have
            // changed and there may have been an interrupt. We read it again to make sure we still
            // have to block (an interrupt might be pending but it will not have executed yet)
            if SIOCNT_.read().tx_full() {
                task::block_on(task::BlockingOn::SerialWritable);
            }
            if SIOCNT_.read().tx_full() {
                panic!("tx still full??");
            }
        });
    }
    SIODATA8.write(data);
}

pub fn write(data: &[u8]) {
    for &byte in data {
        write_byte(byte);
    }
}
