//! Smoltcp ethernet driver for LiteETH instance.
//!
//! Much of the code and implementation ideas are stolen from https://docs.tockos.org/src/litex/liteeth.rs.html#1-307

// God I hate how there's 0 documentation for LiteEth. No explanation of all the
// registers or anything, just here's a bunch of things, you've got to look at
// the gateware to figure out how it works.

// Some notes I'm not too sure where to put:
// - The SRAM writer (i.e. RX DMA) must have the event cleared to mark the slot as ready
// - Do the event registers change with the selected slot? I don't think so. So how
//   are you supposed to know which RX slot to use? In this case I only use one, so I'm
//   not going to care, but it's not obvious.
// - Slots are sized to ethernet MTU (1530), and addressed by the closest log2
//   thing, so 2048 bytes each

// Writer, or RX register blocks
const ETHMAC_SRAM_WRITER_SLOT: u32 = 0x000;
const ETHMAC_SRAM_WRITER_LENGTH: u32 = 0x004;
const ETHMAC_SRAM_WRITER_EV_STATUS: u32 = 0x00c;
const ETHMAC_SRAM_WRITER_EV_PENDING: u32 = 0x010;
const ETHMAC_SRAM_WRITER_EV_ENABLE: u32 = 0x014;

// Reader, or TX register blocks
const ETHMAC_SRAM_READER_START: u32 = 0x018;
const ETHMAC_SRAM_READER_READY: u32 = 0x01c;
const ETHMAC_SRAM_READER_LEVEL: u32 = 0x020;
const ETHMAC_SRAM_READER_SLOT: u32 = 0x024;
const ETHMAC_SRAM_READER_LENGTH: u32 = 0x028;
const ETHMAC_SRAM_READER_EV_STATUS: u32 = 0x02c;
const ETHMAC_SRAM_READER_EV_PENDING: u32 = 0x030;
const ETHMAC_SRAM_READER_EV_ENABLE: u32 = 0x014;

const NUM_RX_SLOTS: u32 = 2;
const NUM_TX_SLOTS: u32 = 2;
const MTU: usize = 1530;
const SLOT_LEN: u32 = 2048;

use crate::{busy_wait, read_reg, write_reg};

use crate::uart::AmlibUart;
use core::fmt::Write;

pub struct LiteEthDevice {
    csr_addr: u32,
    ethmac_addr: u32,
}

pub struct LiteEthTxToken {
    pub csr_addr: u32,
    pub ethmac_addr: u32,
    pub slot: u32,
}

pub struct LiteEthRxToken {
    pub csr_addr: u32,
    pub ethmac_addr: u32,
}

impl LiteEthDevice {
    /// Initialises the device and returns an instance. Unsafe because there are
    /// no checks for other users.
    pub unsafe fn try_init(csr_addr: u32, ethmac_addr: u32) -> Option<Self> {
        // Clear RX event to mark the slot as available
        write_reg(csr_addr + ETHMAC_SRAM_WRITER_EV_PENDING, 1u32);

        // Clear TX event (unsure if necessary)
        write_reg(csr_addr + ETHMAC_SRAM_READER_EV_PENDING, 1u32);

        // Disable event interrupts, we poll, so no use for an interrupt
        write_reg(csr_addr + ETHMAC_SRAM_READER_EV_ENABLE, 0u32);
        write_reg(csr_addr + ETHMAC_SRAM_WRITER_EV_ENABLE, 0u32);

        // Return a new device
        Some(Self { csr_addr, ethmac_addr })
    }
}

impl smoltcp::phy::Device for LiteEthDevice {
    type RxToken<'a> = LiteEthRxToken;
    type TxToken<'a> = LiteEthTxToken;

    fn receive(
        &mut self,
        _timestamp: smoltcp::time::Instant,
    ) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> {
        // Check if available, if so , return a RxToken + a TxToken to slot 1

        unsafe {
            // No data is available
            if read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_WRITER_EV_STATUS) == 0 {
                return None;
            }

            // Due to the fact that I can't check the status of an individual slot, I am going to just make sure
            // level is 0 before I hand out any TX tokens
            if read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_READER_LEVEL) != 0 {
                return None;
            }

            // We have data, and TX slot 1 is ready for something to be potentially transmitted,
            // so we can return valid tokens
            //writeln!(self.uart, "RX tkn").unwrap();
            defmt::trace!("RX Token given");
            Some((
                LiteEthRxToken {
                    csr_addr: self.csr_addr,
                    ethmac_addr: self.ethmac_addr,
                },
                LiteEthTxToken {
                    csr_addr: self.csr_addr,
                    ethmac_addr: self.ethmac_addr,
                    slot: 1,
                },
            ))
        }
    }

    fn transmit(&mut self, _timestamp: smoltcp::time::Instant) -> Option<Self::TxToken<'_>> {
        // Check if slot 0 is ready, if so, return TxToken to slot 0
        unsafe {
            // Due to the fact that I can't check the status of an individual slot, I am going to just make sure
            // level is 0 before I hand out any TX tokens
            if read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_READER_LEVEL) != 0 {
                return None;
            }
        }

        defmt::trace!("TX token given");
        Some(LiteEthTxToken {
            csr_addr: self.csr_addr,
            ethmac_addr: self.ethmac_addr,
            slot: 0,
        })
    }

    fn capabilities(&self) -> smoltcp::phy::DeviceCapabilities {
        use smoltcp::phy::*;
        let mut caps = DeviceCapabilities::default();
        caps.medium = Medium::Ethernet;
        caps.max_transmission_unit = MTU;
        caps.max_burst_size = Some(MTU);

        caps.checksum.udp = Checksum::None;

        caps
    }
}

impl smoltcp::phy::TxToken for LiteEthTxToken {
    fn consume<R, F>(self, len: usize, f: F) -> R
    where
        F: FnOnce(&mut [u8]) -> R,
    {
        let tx_slot_base: u32 = self.ethmac_addr + NUM_RX_SLOTS * SLOT_LEN;
        let tx_slot_addr = tx_slot_base + 0 * SLOT_LEN;
        let tx_slot: &mut [u8] =
            unsafe { core::slice::from_raw_parts_mut(tx_slot_addr as *mut u8, len) };

        unsafe {
            // Wait for it to be ready?
            while read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_READER_READY) == 0 {}
        }

        // Write data to buffer
        let res = f(tx_slot);

        // Write length, and start sending data
        unsafe {
            let level = read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_READER_LEVEL);
            defmt::trace!("level before sending: {}", level);

            // set slot
            write_reg(self.csr_addr + ETHMAC_SRAM_READER_SLOT, 0u32);
            // set length
            write_reg(self.csr_addr + ETHMAC_SRAM_READER_LENGTH, len as u32);

            let slot = read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_READER_SLOT);
            let length = read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_READER_LENGTH);
            defmt::trace!("slot: {}, len: {}, addr: 0x{:08x}", slot, len, tx_slot_addr);

            // send data
            write_reg(self.csr_addr + ETHMAC_SRAM_READER_START, 1u32);

            let level = read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_READER_LEVEL);
            defmt::trace!("level after sending: {}", level);

            // Clear event because why tf not
            write_reg(self.csr_addr + ETHMAC_SRAM_READER_EV_PENDING, 1u32);
        }

        res
    }
}

impl smoltcp::phy::RxToken for LiteEthRxToken {
    fn consume<R, F>(self, f: F) -> R
    where
        F: FnOnce(&mut [u8]) -> R,
    {
        // Read the slot number
        let slot = unsafe {
            read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_WRITER_SLOT)
        };
        // Read the available length
        let len = unsafe {
            read_reg::<u32>(self.csr_addr + ETHMAC_SRAM_WRITER_LENGTH)
        };

        let rx_slot_addr: u32 = self.ethmac_addr + slot * SLOT_LEN;
        let rx_slot: &mut [u8] =
            unsafe { core::slice::from_raw_parts_mut(rx_slot_addr as *mut u8, len as usize) };

        defmt::trace!("rx: len {}, addr: 0x{:08x}", len, rx_slot_addr);
        // Read data from buffer
        let res = f(rx_slot);

        // Clear event to mark slot as available
        unsafe {
            write_reg(self.csr_addr + ETHMAC_SRAM_WRITER_EV_PENDING, 1u32);
        }

        res
    }
}
