use core::future::poll_fn;
use core::task::Poll;

use atomic_polyfill::{compiler_fence, Ordering};

use self::sealed::Instance;
use crate::interrupt;
use crate::interrupt::typelevel::Interrupt;
use crate::peripherals::IPCC;
use crate::rcc::sealed::RccPeripheral;

/// Interrupt handler.
pub struct ReceiveInterruptHandler {}

impl interrupt::typelevel::Handler<interrupt::typelevel::IPCC_C1_RX> for ReceiveInterruptHandler {
    unsafe fn on_interrupt() {
        let regs = IPCC::regs();

        let channels = [
            IpccChannel::Channel1,
            IpccChannel::Channel2,
            IpccChannel::Channel3,
            IpccChannel::Channel4,
            IpccChannel::Channel5,
            IpccChannel::Channel6,
        ];

        // Status register gives channel occupied status. For rx, use cpu1.
        let sr = regs.cpu(1).sr().read();
        regs.cpu(0).mr().modify(|w| {
            for channel in channels {
                if sr.chf(channel as usize) {
                    // If bit is set to 1 then interrupt is disabled; we want to disable the interrupt
                    w.set_chom(channel as usize, true);

                    // There shouldn't be a race because the channel is masked only if the interrupt has fired
                    IPCC::state().rx_waker_for(channel).wake();
                }
            }
        })
    }
}

pub struct TransmitInterruptHandler {}

impl interrupt::typelevel::Handler<interrupt::typelevel::IPCC_C1_TX> for TransmitInterruptHandler {
    unsafe fn on_interrupt() {
        let regs = IPCC::regs();

        let channels = [
            IpccChannel::Channel1,
            IpccChannel::Channel2,
            IpccChannel::Channel3,
            IpccChannel::Channel4,
            IpccChannel::Channel5,
            IpccChannel::Channel6,
        ];

        // Status register gives channel occupied status. For tx, use cpu0.
        let sr = regs.cpu(0).sr().read();
        regs.cpu(0).mr().modify(|w| {
            for channel in channels {
                if !sr.chf(channel as usize) {
                    // If bit is set to 1 then interrupt is disabled; we want to disable the interrupt
                    w.set_chfm(channel as usize, true);

                    // There shouldn't be a race because the channel is masked only if the interrupt has fired
                    IPCC::state().tx_waker_for(channel).wake();
                }
            }
        });
    }
}

#[non_exhaustive]
#[derive(Clone, Copy, Default)]
pub struct Config {
    // TODO: add IPCC peripheral configuration, if any, here
    // reserved for future use
}

#[derive(Debug, Clone, Copy)]
#[repr(C)]
pub enum IpccChannel {
    Channel1 = 0,
    Channel2 = 1,
    Channel3 = 2,
    Channel4 = 3,
    Channel5 = 4,
    Channel6 = 5,
}

pub struct Ipcc;

impl Ipcc {
    pub fn enable(_config: Config) {
        IPCC::enable();
        IPCC::reset();
        IPCC::set_cpu2(true);

        _configure_pwr();

        let regs = IPCC::regs();

        regs.cpu(0).cr().modify(|w| {
            w.set_rxoie(true);
            w.set_txfie(true);
        });

        // enable interrupts
        crate::interrupt::typelevel::IPCC_C1_RX::unpend();
        crate::interrupt::typelevel::IPCC_C1_TX::unpend();

        unsafe { crate::interrupt::typelevel::IPCC_C1_RX::enable() };
        unsafe { crate::interrupt::typelevel::IPCC_C1_TX::enable() };
    }

    /// Send data to an IPCC channel. The closure is called to write the data when appropriate.
    pub async fn send(channel: IpccChannel, f: impl FnOnce()) {
        let regs = IPCC::regs();

        Self::flush(channel).await;

        f();

        compiler_fence(Ordering::SeqCst);

        trace!("ipcc: ch {}: send data", channel as u8);
        regs.cpu(0).scr().write(|w| w.set_chs(channel as usize, true));
    }

    /// Wait for the tx channel to become clear
    pub async fn flush(channel: IpccChannel) {
        let regs = IPCC::regs();

        // This is a race, but is nice for debugging
        if regs.cpu(0).sr().read().chf(channel as usize) {
            trace!("ipcc: ch {}: wait for tx free", channel as u8);
        }

        poll_fn(|cx| {
            IPCC::state().tx_waker_for(channel).register(cx.waker());
            // If bit is set to 1 then interrupt is disabled; we want to enable the interrupt
            regs.cpu(0).mr().modify(|w| w.set_chfm(channel as usize, false));

            compiler_fence(Ordering::SeqCst);

            if !regs.cpu(0).sr().read().chf(channel as usize) {
                // If bit is set to 1 then interrupt is disabled; we want to disable the interrupt
                regs.cpu(0).mr().modify(|w| w.set_chfm(channel as usize, true));

                Poll::Ready(())
            } else {
                Poll::Pending
            }
        })
        .await;
    }

    /// Receive data from an IPCC channel. The closure is called to read the data when appropriate.
    pub async fn receive<R>(channel: IpccChannel, mut f: impl FnMut() -> Option<R>) -> R {
        let regs = IPCC::regs();

        loop {
            // This is a race, but is nice for debugging
            if !regs.cpu(1).sr().read().chf(channel as usize) {
                trace!("ipcc: ch {}: wait for rx occupied", channel as u8);
            }

            poll_fn(|cx| {
                IPCC::state().rx_waker_for(channel).register(cx.waker());
                // If bit is set to 1 then interrupt is disabled; we want to enable the interrupt
                regs.cpu(0).mr().modify(|w| w.set_chom(channel as usize, false));

                compiler_fence(Ordering::SeqCst);

                if regs.cpu(1).sr().read().chf(channel as usize) {
                    // If bit is set to 1 then interrupt is disabled; we want to disable the interrupt
                    regs.cpu(0).mr().modify(|w| w.set_chfm(channel as usize, true));

                    Poll::Ready(())
                } else {
                    Poll::Pending
                }
            })
            .await;

            trace!("ipcc: ch {}: read data", channel as u8);

            match f() {
                Some(ret) => return ret,
                None => {}
            }

            trace!("ipcc: ch {}: clear rx", channel as u8);
            compiler_fence(Ordering::SeqCst);
            // If the channel is clear and the read function returns none, fetch more data
            regs.cpu(0).scr().write(|w| w.set_chc(channel as usize, true));
        }
    }
}

impl sealed::Instance for crate::peripherals::IPCC {
    fn regs() -> crate::pac::ipcc::Ipcc {
        crate::pac::IPCC
    }

    fn set_cpu2(enabled: bool) {
        crate::pac::PWR.cr4().modify(|w| w.set_c2boot(enabled));
    }

    fn state() -> &'static self::sealed::State {
        static STATE: self::sealed::State = self::sealed::State::new();
        &STATE
    }
}

pub(crate) mod sealed {
    use embassy_sync::waitqueue::AtomicWaker;

    use super::*;

    pub struct State {
        rx_wakers: [AtomicWaker; 6],
        tx_wakers: [AtomicWaker; 6],
    }

    impl State {
        pub const fn new() -> Self {
            const WAKER: AtomicWaker = AtomicWaker::new();

            Self {
                rx_wakers: [WAKER; 6],
                tx_wakers: [WAKER; 6],
            }
        }

        pub const fn rx_waker_for(&self, channel: IpccChannel) -> &AtomicWaker {
            match channel {
                IpccChannel::Channel1 => &self.rx_wakers[0],
                IpccChannel::Channel2 => &self.rx_wakers[1],
                IpccChannel::Channel3 => &self.rx_wakers[2],
                IpccChannel::Channel4 => &self.rx_wakers[3],
                IpccChannel::Channel5 => &self.rx_wakers[4],
                IpccChannel::Channel6 => &self.rx_wakers[5],
            }
        }

        pub const fn tx_waker_for(&self, channel: IpccChannel) -> &AtomicWaker {
            match channel {
                IpccChannel::Channel1 => &self.tx_wakers[0],
                IpccChannel::Channel2 => &self.tx_wakers[1],
                IpccChannel::Channel3 => &self.tx_wakers[2],
                IpccChannel::Channel4 => &self.tx_wakers[3],
                IpccChannel::Channel5 => &self.tx_wakers[4],
                IpccChannel::Channel6 => &self.tx_wakers[5],
            }
        }
    }

    pub trait Instance: crate::rcc::RccPeripheral {
        fn regs() -> crate::pac::ipcc::Ipcc;
        fn set_cpu2(enabled: bool);
        fn state() -> &'static State;
    }
}

fn _configure_pwr() {
    // TODO: move this to RCC

    let pwr = crate::pac::PWR;
    let rcc = crate::pac::RCC;

    rcc.cfgr().modify(|w| w.set_stopwuck(true));

    pwr.cr1().modify(|w| w.set_dbp(true));
    pwr.cr1().modify(|w| w.set_dbp(true));

    // configure LSE
    rcc.bdcr().modify(|w| w.set_lseon(true));

    // select system clock source = PLL
    // set PLL coefficients
    // m: 2,
    // n: 12,
    // r: 3,
    // q: 4,
    // p: 3,
    let src_bits = 0b11;
    let pllp = (3 - 1) & 0b11111;
    let pllq = (4 - 1) & 0b111;
    let pllr = (3 - 1) & 0b111;
    let plln = 12 & 0b1111111;
    let pllm = (2 - 1) & 0b111;
    rcc.pllcfgr().modify(|w| {
        w.set_pllsrc(src_bits);
        w.set_pllm(pllm);
        w.set_plln(plln);
        w.set_pllr(pllr);
        w.set_pllp(pllp);
        w.set_pllpen(true);
        w.set_pllq(pllq);
        w.set_pllqen(true);
    });
    // enable PLL
    rcc.cr().modify(|w| w.set_pllon(true));
    rcc.cr().write(|w| w.set_hsion(false));
    // while !rcc.cr().read().pllrdy() {}

    // configure SYSCLK mux to use PLL clocl
    rcc.cfgr().modify(|w| w.set_sw(0b11));

    // configure CPU1 & CPU2 dividers
    rcc.cfgr().modify(|w| w.set_hpre(0)); // not divided
    rcc.extcfgr().modify(|w| {
        w.set_c2hpre(0b1000); // div2
        w.set_shdhpre(0); // not divided
    });

    // apply APB1 / APB2 values
    rcc.cfgr().modify(|w| {
        w.set_ppre1(0b000); // not divided
        w.set_ppre2(0b000); // not divided
    });

    // TODO: required
    // set RF wake-up clock = LSE
    rcc.csr().modify(|w| w.set_rfwkpsel(0b01));

    // set LPTIM1 & LPTIM2 clock source
    rcc.ccipr().modify(|w| {
        w.set_lptim1sel(0b00); // PCLK
        w.set_lptim2sel(0b00); // PCLK
    });
}
