//! Interface between the NRF24 sensors and MQTT/InfluxDB.
//!
//! TODO: Document configuration and hardware setup.

use std::env;
use std::sync::Arc;
use std::time::{Duration, Instant};

use log::{error, info};
use protocol::{GetValues, Packet, Report, Value, Values};
use rand::{rngs::ThreadRng, Rng};
use tokio::sync::Mutex;
use tokio::time::delay_for;

use current::CurrentValues;
use publish::Publish;
use radio::{Radio, RadioConfig};
use tsdb::TimeSeriesDatabase;

mod current;
mod publish;
mod radio;
mod spi;
mod tsdb;

#[tokio::main]
async fn main() {
    env::set_var(
        "RUST_LOG",
        env::var_os("RUST_LOG").unwrap_or_else(|| "info".into()),
    );
    env_logger::init();

    // Initialize radio, MQTT and InfluxDB.
    let radio_config = RadioConfig{
        // TODO: Configure pins.
    };
    let (radio, mut incoming_packets) = Radio::init(radio_config)
        .await
        .expect("radio initialization failed");
    let tsdb = TimeSeriesDatabase::init();
    let publish = Publish::init();

    let mut app = Application {
        radio: Arc::new(Mutex::new(radio)),
        publish: Arc::new(Mutex::new(publish)),
        tsdb: Arc::new(Mutex::new(tsdb)),
        current: CurrentValues::new(),
        rng: rand::thread_rng(),
    };

    // Wait for packets and handle the corresponding requests.
    loop {
        let (device_id, packet) = incoming_packets
            .recv()
            .await
            .expect("unexpected end of packet stream");
        app.handle_packet(device_id, packet).await;
    }
}

/// Type which implements packet handling and responds to requests from devices.
struct Application {
    radio: Arc<Mutex<Radio>>,
    publish: Arc<Mutex<Publish>>,
    tsdb: Arc<Mutex<TimeSeriesDatabase>>,
    current: CurrentValues,
    rng: ThreadRng,
}

impl Application {
    /// Handles a single packet from a device and either sends the data to MQTT/InfluxDB or
    /// generates a response to the device.
    async fn handle_packet(&mut self, device_id: u8, packet: Packet) {
        // TODO: More packet types to query timeseries database.
        match packet {
            Packet::GetSalt => {
                self.handle_get_salt(device_id).await;
            }
            Packet::Salt(_) => {
                error!("Received Salt packet from device, ignoring.");
            }
            Packet::Report(payload) => {
                self.handle_report(device_id, payload).await;
            }
            Packet::GetValues(payload) => {
                self.handle_get_values(device_id, payload).await;
            }
            Packet::Values(_payload) => {
                error!("received Values packet from device.");
            }
        }
    }

    async fn handle_get_salt(&mut self, device_id: u8) {
        delay_for(Duration::from_millis(1)).await;
        let salt = (self.rng.gen::<u64>() & !0xff & !(1 << 63)) | device_id as u64;
        // TODO: Store the salt as the expected salt in Radio.
        self.radio
            .lock()
            .await
            .send_packet_async(device_id, Packet::Salt(salt));
    }

    async fn handle_report(&mut self, device_id: u8, report: Report) {
        let location = radio::get_device_location(device_id);
        let count = report.count as usize;
        // Update local copy of current values.
        for i in 0..count {
            match report.values[i] {
                Value::Invalid => {}
                Value::Time(_) => {
                    error!("device tried to report a time");
                }
                Value::Temperature(temperature) => {
                    let temperature = temperature as f32 / 10.0;
                    info!("{:?} temperature: {} °C", location, temperature);
                }
                Value::Pressure(pressure) => {
                    let pressure = pressure as f32 / 100.0;
                    info!("{:?} pressure: {} HPa", location, pressure);
                }
                Value::Humidity(humidity) => {
                    let humidity = humidity as f32 / 100.0;
                    info!("{:?} humidity: {}%", location, humidity);
                }
            }
            self.current.set(location, report.values[i]);
        }

        // Send values via MQTT.
        let publish = self.publish.clone();
        let r = report.clone();
        tokio::spawn(async move {
            let mut publish = publish.lock().await;
            publish.publish(location, &r.values[0..count]).await;
        });

        // Insert values into TSDB.
        let tsdb = self.tsdb.clone();
        tokio::spawn(async move {
            let mut tsdb = tsdb.lock().await;
            tsdb.insert(Instant::now(), location, &report.values[0..count])
                .await;
        });
    }

    async fn handle_get_values(&mut self, device_id: u8, request: GetValues) {
        let count = request.count as usize;
        let mut response = Values {
            count: 0,
            location: request.location,
            values: [Value::Invalid; 8],
        };
        for i in 0..count {
            match self.current.get(request.location, request.types_[i]) {
                Some(value) => {
                    // During deserialization, we made sure that count can never be larger than 7.
                    response.values[response.count as usize] = value;
                    response.count += 1;
                }
                None => {
                    // TODO: Tell the sender that the value was not found?
                }
            }
        }
        self.radio
            .lock()
            .await
            .send_packet_async(device_id, Packet::Values(response));
    }
}

/*fn run_mqtt_connection(updates: &mpsc::Receiver<SensorUpdate>) -> Result<(), Error> {
    let mut m = MQTT::connect("127.0.0.1:1883")?;

    loop {
        let update = updates.recv().unwrap();
        let location = match update.location {
            0 => "livingroom",
            _ => {
                error!("Unknown location in sensor update.");
                continue;
            }
        };
        for value in update.data.iter() {
            match value {
                SensorData::Temperature(value) => {
                    m.publish(
                        &format!("gottschlag/{}/temperature", location),
                        &format!("{}", value),
                    )?;
                }
                SensorData::Humidity(value) => {
                    m.publish(
                        &format!("gottschlag/{}/humidity", location),
                        &format!("{}", value),
                    )?;
                }
                SensorData::Pressure(value) => {
                    m.publish(
                        &format!("gottschlag/{}/pressure", location),
                        &format!("{}", value),
                    )?;
                }
            }
        }
    }
}*/
