//! Wrapper around the NRF24 driver which provides tokio-compatible asynchronous interfaces.

use std::convert::TryInto;
use std::io;
use std::time::Duration;

use embedded_hal::blocking::spi::Transfer;
use embedded_hal::digital::v2::OutputPin;
use embedded_nrf24l01::{Configuration, CrcMode, DataRate, RxMode, NRF24L01};
use futures_util::stream::StreamExt;
use gpio_cdev::{AsyncLineEventHandle, Chip, EventRequestFlags, LineRequestFlags};
use linux_embedded_hal::spidev::{SpiModeFlags, Spidev, SpidevOptions};
use linux_embedded_hal::sysfs_gpio::{Direction, Error as GpioError};
use linux_embedded_hal::Pin;
use log::{error, info};
use protocol::{Location, Packet};
use rand::Rng;
use tokio::select;
use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::time::delay_for;

use super::spi::EmbeddedHalSpidev;

const DISPLAY_ID: u8 = 0x20;
const DISPLAY_KEY: [u8; 16] = include!("../../../common/display_key.txt");
const WEATHER_STATION_0_ID: u8 = 0x30;
const WEATHER_STATION_0_KEY: [u8; 16] = include!("../../../common/weather_station_0_key.txt");

/// Hardware configuration.
pub struct RadioConfig {
    // TODO
}

/// Wrapper around the NRF24 radio driver.
pub struct Radio {
    tx: UnboundedSender<(u8, Packet)>,
}

impl Radio {
    /// Initializes the radio and returns both radio and the stream of incoming packets.
    pub async fn init(
        config: RadioConfig,
    ) -> Result<(Radio, UnboundedReceiver<(u8, Packet)>), RadioError> {
        info!("Initializing radio...");
        let (tx_send, tx_recv) = mpsc::unbounded_channel();
        let (rx_send, rx_recv) = mpsc::unbounded_channel();
        let mut task = RadioTask {
            config,
            nrf24: None,
            irq: None,
            tx: tx_recv,
            rx: rx_send,
        };
        task.init().await?;
        info!("radio initialized.");

        // Start a task for packet handling.
        tokio::spawn(async move {
            task.run().await;
        });

        Ok((Radio { tx: tx_send }, rx_recv))
    }

    /// Sends a packet to the specified device.
    ///
    /// The function returns immediately, and the packet might or might not be sent successfully.
    ///
    /// # Arguments
    ///
    /// * `device_id` - the ID of the targeted device
    /// * `packet` - the packet to send
    pub fn send_packet_async(&mut self, device_id: u8, packet: Packet) {
        self.tx.send((device_id, packet)).unwrap();
    }
}

struct RadioTask {
    config: RadioConfig,
    nrf24: Option<RxMode<NRF24L01<GpioError, Pin, Pin, EmbeddedHalSpidev>>>,
    irq: Option<AsyncLineEventHandle>,
    tx: UnboundedReceiver<(u8, Packet)>,
    rx: UnboundedSender<(u8, Packet)>,
}

impl RadioTask {
    async fn init(&mut self) -> Result<(), RadioError> {
        // The NRF module is connected as follows:
        // - CE: PA1
        // - CS: PG8
        // - IRQ: PG9
        // - MOSI: PC0
        // - MISO: PC1
        // - SCK: PC2
        // - Power (active low): PG11

        // Configure the GPIOs.
        let pwr_nr = get_pin_number('G', 11);
        let mut pwr = Pin::new(pwr_nr);
        pwr.export().unwrap();
        pwr.set_direction(Direction::Out).unwrap();

        let ce_nr = get_pin_number('A', 1);
        let mut ce = Pin::new(ce_nr);
        ce.export().unwrap();
        ce.set_direction(Direction::Out).unwrap();
        ce.set_high().unwrap();
        let cs_nr = get_pin_number('G', 8);
        let mut cs = Pin::new(cs_nr);
        cs.export().unwrap();
        cs.set_direction(Direction::Out).unwrap();
        cs.set_high().unwrap();
        let mut chip = Chip::new("/dev/gpiochip0")?;
        let irq_nr = get_pin_number('G', 9);
        let line = chip.get_line(irq_nr as u32)?;
        let irq = AsyncLineEventHandle::new(line.events(
            LineRequestFlags::INPUT,
            EventRequestFlags::BOTH_EDGES,
            "gpioevents",
        )?)?;

        // Configure SPI.
        let mut spi = Spidev::open("/dev/spidev0.0").unwrap();
        let options = SpidevOptions::new()
            .bits_per_word(8)
            .max_speed_hz(500_000)
            .mode(SpiModeFlags::SPI_MODE_0)
            .build();
        spi.configure(&options).unwrap();
        let mut spi = EmbeddedHalSpidev::from(spi);

        // HACK: Cycle power until transfers to the module seem to work.
        loop {
            cs.set_high()?;
            pwr.set_high()?;
            delay_for(Duration::from_millis(100)).await;
            pwr.set_low()?;
            delay_for(Duration::from_millis(50)).await;

            cs.set_low()?;
            let mut read_aw = [0x03, 0x00];
            if spi.transfer(&mut read_aw).is_ok() {
                // Correct address field width?
                if read_aw[1] >= 0x1 && read_aw[1] <= 0x3 {
                    break;
                }
            }
            info!("radio not yet responding correctly ({:x})...", read_aw[1]);
            delay_for(Duration::from_millis(1000)).await;
        }

        // Initialize the radio module.
        let mut nrf24 = NRF24L01::new(ce, cs, spi)?;
        nrf24.set_frequency(0x32)?;
        nrf24.set_rf(DataRate::R2Mbps, 3)?;
        nrf24.set_crc(Some(CrcMode::OneByte))?;
        nrf24.set_auto_retransmit(250, 3)?;
        nrf24.set_pipes_rx_enable(&[true, true, false, false, false, false])?;
        nrf24.set_rx_addr(0, &[0xB3, 0xB3, 0xB3, 0xB3, 0x00])?;
        nrf24.set_rx_addr(1, &[0xB3, 0xB3, 0xB3, 0xB3, 0x00])?;
        nrf24.flush_rx()?;
        nrf24.flush_tx()?;
        nrf24.set_auto_ack(&[true; 6])?;
        info!("auto ack: {:?}", nrf24.get_auto_ack()?);
        nrf24.set_pipes_rx_lengths(&[None; 6])?;

        self.nrf24 = Some(nrf24.rx().unwrap());
        self.irq = Some(irq);

        Ok(())
    }

    async fn run(&mut self) {
        loop {
            let e = self.run_with_initialized_radio().await;
            error!("Error in radio task: {:?}", e);
            self.nrf24 = None;
            // Try to reinitialize the radio.
            loop {
                info!("Trying to reinitialize radio: {:?}", e);
                match self.init().await {
                    Ok(_) => break,
                    Err(e) => error!("Radio reinitialization failed: {:?}", e),
                }
                delay_for(Duration::from_secs(3)).await;
            }
        }
    }

    async fn run_with_initialized_radio(&mut self) -> Result<(), RadioError> {
        // Wait for an RX interrupt of until we have to send a packet.
        loop {
            select! {
                _pin_event = self.irq.as_mut().unwrap().next() => {
                    // Try to receive until there are no packets left in the RX fifo and until the
                    // interrupt pin is high again.
                    self.receive_packets().await?;
                },
                packet = self.tx.next() => {
                    let (device_id, packet) = packet.unwrap();
                    // Switch to TX mode and send the packet.
                    self.send_packet(device_id, packet).await?;
                },
            }
        }
    }

    async fn receive_packets(&mut self) -> Result<(), RadioError> {
        loop {
            let rx = self.nrf24.as_mut().unwrap();
            let pipe = match rx.can_read()? {
                Some(p) => p,
                None => return Ok(()),
            };
            let payload = rx.read()?;
            info!(
                "packet received on pipe {}: {:x?}, {}",
                pipe,
                payload.as_ref(),
                payload.len()
            );
            if payload.len() != 32 {
                return Ok(());
            }

            let mut payload: [u8; 32] = payload.as_ref().try_into().unwrap();
            self.handle_packet(&mut payload).await;
        }
    }

    async fn handle_packet(&mut self, packet: &mut [u8]) {
        // Get the key of the device.
        let device_id = packet[0];
        let key = match get_key(device_id) {
            Some(k) => k,
            None => {
                info!("packet from unknown device {:02x}", device_id);
                return;
            }
        };

        // Decode the packet.
        let packet = match Packet::decrypt_and_decode(key, packet) {
            Err(e) => {
                info!(
                    "invalid packet from device {:02x}, error {:?}",
                    device_id, e
                );
                return;
            }
            Ok(p) => p,
        };
        // TODO: For all packets except for salt requests, check whether the
        // salt was incremented to prevent replay attacks.
        // TODO: Also check whether the device bit in the salt is 0.

        info!("packet from {}: {:?}", device_id, packet);

        self.rx.send((device_id, packet)).unwrap();
    }

    async fn send_packet(&mut self, device_id: u8, packet: Packet) -> Result<(), RadioError> {
        let salt = rand::thread_rng().gen::<u64>();
        // TODO: Fix salt (device bit, device id).
        let mut tx = self
            .nrf24
            .take()
            .unwrap()
            .standby()
            .tx()
            .map_err(|(_, e)| RadioError::Radio(e))?;
        let mut encoded = [0u8; 32];
        let key = get_key(device_id);

        info!("sending packet: {:?}", packet);
        if packet.encode_and_encrypt(key.unwrap(), salt, &mut encoded) {
            tx.set_tx_addr(&[0xB3, 0xB3, 0xB3, 0xB3, device_id])
                .unwrap();
            tx.set_rx_addr(0, &[0xB3, 0xB3, 0xB3, 0xB3, device_id])
                .unwrap();
            tx.send(&encoded).unwrap();

            // Wait for the packet to be sent and check whether it was acknowledged.
            loop {
                delay_for(Duration::from_millis(1)).await;
                match tx.poll_send() {
                    Ok(status) => {
                        if !status {
                            error!("packet lost.");
                        }
                        break;
                    }
                    Err(nb::Error::WouldBlock) => {
                        // TODO: Wait for interrupt.
                    }
                    Err(nb::Error::Other(e)) => return Err(e.into()),
                }
            }
        } else {
            info!("could not encode packet {:?}", packet);
        }
        self.nrf24 = Some(
            tx.standby()
                .map_err(RadioError::Radio)?
                .rx()
                .map_err(|(_, e)| RadioError::Radio(e))?,
        );
        Ok(())
    }
}

#[derive(thiserror::Error, Debug)]
pub enum RadioError {
    #[error("I/O error")]
    Io(#[from] io::Error),
    #[error("GPIO error")]
    Gpio(#[from] GpioError),
    #[error("GPIO error (gpio-cdev)")]
    GpioCdev(#[from] gpio_cdev::errors::Error),
    #[error("radio error: {0:?}")]
    Radio(embedded_nrf24l01::Error<std::io::Error>),
}

impl From<embedded_nrf24l01::Error<std::io::Error>> for RadioError {
    fn from(e: embedded_nrf24l01::Error<std::io::Error>) -> Self {
        RadioError::Radio(e)
    }
}

pub fn get_device_location(device_id: u8) -> Location {
    match device_id {
        DISPLAY_ID => Location::Bedroom,
        WEATHER_STATION_0_ID => Location::Livingroom,
        _ => Location::Livingroom,
    }
}

fn get_key(device_id: u8) -> Option<&'static [u8]> {
    match device_id {
        DISPLAY_ID => Some(&DISPLAY_KEY),
        WEATHER_STATION_0_ID => Some(&WEATHER_STATION_0_KEY),
        _ => None,
    }
}

fn get_pin_number(c: char, n: u64) -> u64 {
    (c as u64 - 'A' as u64) * 32 + n
}
