use core::iter::once;
use embassy_time::{Duration, Timer};
use esp_hal::gpio::{Flex, Level, Output};

async fn spi_delay() {
    Timer::after(Duration::from_micros(1)).await;
}

async fn spi_dummy_bit(sck: &mut Output<'_>) {
    sck.set_low();
    spi_delay().await;
    sck.set_high();
    spi_delay().await;
}

async fn spi_write_bit(bit: bool, mosi: &mut Flex<'_>, sck: &mut Output<'_>) {
    mosi.set_level(if bit { Level::High } else { Level::Low });
    sck.set_low();
    spi_delay().await;
    sck.set_high();
    spi_delay().await;
}

async fn spi_read_bit(mosi: &mut Flex<'_>, sck: &mut Output<'_>) -> bool {
    sck.set_low();
    spi_delay().await;
    sck.set_high();
    spi_delay().await;
    mosi.is_high()
}

async fn spi_write_bits(
    bits: impl Iterator<Item = bool>,
    mosi: &mut Flex<'_>,
    sck: &mut Output<'_>,
) {
    for bit in bits {
        spi_write_bit(bit, mosi, sck).await;
    }
}

async fn spi_write_word(is_param: bool, data: u8, mosi: &mut Flex<'_>, sck: &mut Output<'_>) {
    assert!(sck.is_set_high());
    spi_write_bits(
        once(is_param).chain((0..8).map(|i| (data >> i) & 1 != 0).rev()),
        mosi,
        sck,
    )
    .await;
}

pub async fn spi_write(
    command: u8,
    args: impl IntoIterator<Item = u8>,
    mosi: &mut Flex<'_>,
    sck: &mut Output<'_>,
    cs: &mut Output<'_>,
) {
    cs.set_low();
    spi_write_word(false, command, mosi, sck).await;

    for arg in args {
        spi_write_word(true, arg, mosi, sck).await;
    }

    cs.set_high();
}

#[expect(unused)]
pub async fn spi_read(
    command: u8,
    dummy_cycle: bool,
    mosi: &mut Flex<'_>,
    sck: &mut Output<'_>,
    cs: &mut Output<'_>,
    output_buffer: &mut [u8],
) {
    output_buffer.fill(0);

    cs.set_low();
    spi_write_word(false, command, mosi, sck).await;

    mosi.set_output_enable(false);
    mosi.set_input_enable(true);

    if dummy_cycle {
        spi_dummy_bit(sck).await;
    }

    for output_byte in output_buffer {
        for i in (0..8).rev() {
            if spi_read_bit(mosi, sck).await {
                *output_byte |= 1 << i;
            }
        }
    }

    mosi.set_input_enable(false);
    mosi.set_output_enable(true);

    cs.set_high();
}
