//! USBD peripheral

use core::sync::atomic::{self, Ordering};

use crate::{
    errata,
    peripheral::{POWER, USBD},
};

/// Endpoint IN 0
pub struct Ep0In {
    buffer: &'static mut [u8; 64],
    busy: bool,
}

impl Ep0In {
    /// # Safety
    /// Must be created at most once (singleton)
    pub(crate) unsafe fn new(buffer: &'static mut [u8; 64]) -> Self {
        Self {
            buffer,
            busy: false,
        }
    }

    /// Starts a data transfer over endpoint 0
    ///
    /// # Panics
    ///
    /// - This function panics if the last transfer was not finished by calling the `end` function
    /// - This function panics if `bytes` is larger than the maximum packet size (64 bytes)
    pub fn start(&mut self, bytes: &[u8], usbd: &USBD) {
        assert!(!self.busy, "EP0IN: last transfer has not completed");
        assert!(
            bytes.len() <= self.buffer.len(),
            "EP0IN: multi-packet data transfers are not supported"
        );

        let n = bytes.len();
        self.buffer[..n].copy_from_slice(bytes);

        // use a "shortcut" to issue a status stage after the data transfer is complete
        usbd.shorts
            .modify(|_, w| w.ep0datadone_ep0status().set_bit());
        usbd.epin0
            .maxcnt
            .write(|w| unsafe { w.maxcnt().bits(n as u8) });
        usbd.epin0
            .ptr
            .write(|w| unsafe { w.ptr().bits(self.buffer.as_ptr() as u32) });

        self.busy = true;

        log::info!("EP0IN: start {}B transfer", n);

        // start DMA transfer
        dma_start();
        usbd.tasks_startepin[0].write(|w| w.tasks_startepin().set_bit());
    }

    /// Completes a data transfer
    ///
    /// This function must be called after the EP0DATADONE event is raised
    ///
    /// # Panics
    ///
    /// This function panics if called before `start` or before the EP0DATADONE event is raised by
    /// the hardware
    pub fn end(&mut self, usbd: &USBD) {
        if usbd.events_ep0datadone.read().bits() == 0 {
            panic!("Ep0In.end called before the EP0DATADONE event was raised");
        } else {
            // DMA transfer complete
            dma_end();
            usbd.events_ep0datadone.reset();

            self.busy = false;
            log::info!("EP0IN: transfer done");
        }
    }
}

// memory barrier to synchronize the start of a DMA transfer (which will run in parallel) with the
// caller's memory operations
//
// This function call *must* be *followed* by a memory *store* operation. Memory operations that
// *precede* this function call will *not* be moved, by the compiler or the instruction pipeline, to
// *after* the function call.
fn dma_start() {
    atomic::fence(Ordering::Release);
}

// memory barrier to synchronize the end of a DMA transfer (which ran in parallel) to the caller's
// memory operations
//
// This function call *must* be *preceded* by a memory *load* operation. Memory operations that
// *follow* this function call will *not* be moved, by the compiler or the instruction pipeline, to
// *before* the function call.
fn dma_end() {
    atomic::fence(Ordering::Acquire);
}

/// Initializes the USBD peripheral
// NOTE will be called from user code; at that point the high frequency clock source has already
// been configured to use to the external crystal
// Reference: section 6.35.4 of the nRF52840 Product Specification
pub fn init(power: POWER, usbd: &USBD) {
    let mut once = true;

    // wait until the USB cable has been connected
    while power.events_usbdetected.read().bits() == 0 {
        if once {
            log::info!("waiting for USB connection on port J3");
            once = false;
        }

        continue;
    }
    power.events_usbdetected.reset();

    // workaround silicon bug
    unsafe { errata::e187a() }
    // enable the USB peripheral
    usbd.enable.write(|w| w.enable().set_bit());

    // wait for the peripheral to signal it has reached the READY state
    while usbd.eventcause.read().ready().bit_is_clear() {
        continue;
    }
    // write 1 to clear the flag
    usbd.eventcause.write(|w| w.ready().set_bit());

    // if EVENTCAUSE is all zeroes then also clear the USBEVENT register
    if usbd.eventcause.read().bits() == 0 {
        usbd.events_usbevent.reset();
    }

    // complete the silicon bug workaround
    unsafe { errata::e187b() }

    // also need to wait for the USB power supply regulator to stabilize
    while power.events_usbpwrrdy.read().bits() == 0 {
        continue;
    }
    power.events_usbpwrrdy.reset();

    // before returning unmask the relevant interrupts
    usbd.intenset.write(|w| {
        w.ep0datadone().set_bit();
        w.ep0setup().set_bit();
        w.usbreset().set_bit()
    });

    // enable the D+ line pull-up
    usbd.usbpullup.write(|w| w.connect().set_bit());
}

/// Stalls endpoint 0
pub fn ep0stall(usbd: &USBD) {
    usbd.tasks_ep0stall.write(|w| w.tasks_ep0stall().set_bit());
}

/// USBD.EVENTS registers mapped to an enum
#[derive(Debug)]
pub enum Event {
    /// `EVENTS_USBRESET` register was active
    UsbReset,

    /// `EVENTS_EP0DATADONE` register was active
    UsbEp0DataDone,

    /// `EVENTS_EP0SETUP` register was active
    UsbEp0Setup,
}

/// Returns the next unhandled USB event; returns none if there's no event to handle
///
/// NOTE this function will clear the corresponding the EVENT register (*) so the caller should
/// handle the returned event properly. Expect for USBEVENT and EP0DATADONE
pub fn next_event(usbd: &USBD) -> Option<Event> {
    if usbd.events_usbreset.read().bits() != 0 {
        usbd.events_usbreset.reset();

        return Some(Event::UsbReset);
    }

    if usbd.events_ep0datadone.read().bits() != 0 {
        // this will be cleared by the `Ep0In.end` method
        // usbd.events_ep0datadone.reset();

        return Some(Event::UsbEp0DataDone);
    }

    if usbd.events_ep0setup.read().bits() != 0 {
        usbd.events_ep0setup.reset();

        return Some(Event::UsbEp0Setup);
    }

    None
}

/// Reads the BMREQUESTTYPE register and returns the 8-bit BMREQUESTTYPE component of a setup packet
pub fn bmrequesttype(usbd: &USBD) -> u8 {
    // read the 32-bit register and extract the least significant byte
    // (the alternative is to read the 3 bitfields of the register and merge them into one byte)
    usbd.bmrequesttype.read().bits() as u8
}

/// Reads the BREQUEST register and returns the 8-bit BREQUEST component of a setup packet
pub fn brequest(usbd: &USBD) -> u8 {
    usbd.brequest.read().brequest().bits()
}

/// Reads the WLENGTHL and WLENGTHH registers and returns the 16-bit WLENGTH component of a setup packet
pub fn wlength(usbd: &USBD) -> u16 {
    u16::from(usbd.wlengthl.read().wlengthl().bits())
        | u16::from(usbd.wlengthh.read().wlengthh().bits()) << 8
}

/// Reads the WINDEXL and WINDEXH registers and returns the 16-bit WINDEX component of a setup packet
pub fn windex(usbd: &USBD) -> u16 {
    u16::from(usbd.windexl.read().windexl().bits())
        | u16::from(usbd.windexh.read().windexh().bits()) << 8
}

/// Reads the WVALUEL and WVALUEH registers and returns the 16-bit WVALUE component of a setup packet
pub fn wvalue(usbd: &USBD) -> u16 {
    u16::from(usbd.wvaluel.read().wvaluel().bits())
        | u16::from(usbd.wvalueh.read().wvalueh().bits()) << 8
}
