use crate::frame::compute_perceived_lightness_percent;
use itertools::Itertools;
use std::cell::RefCell;
use std::collections::HashMap;
use std::error::Error;
use std::sync::mpsc::{Receiver, Sender};
use std::thread;
use std::time::Duration;
use v4l::buffer::Type;
use v4l::io::mmap::Stream;
use v4l::io::traits::CaptureStream;
use v4l::video::Capture;
use v4l::{Device, FourCC};

const DEFAULT_LUX: u64 = 100;
const WAITING_SLEEP_MS: u64 = 2000;

pub struct Webcam {
    webcam_tx: Sender<u64>,
    video: usize,
}

impl Webcam {
    pub fn new(webcam_tx: Sender<u64>, video: usize) -> Self {
        Self { webcam_tx, video }
    }

    pub fn run(&mut self) {
        loop {
            self.step();
        }
    }

    fn step(&mut self) {
        if let Ok((rgbs, pixels)) = self.frame() {
            let lux = compute_perceived_lightness_percent(&rgbs, false, pixels) as u64;

            self.webcam_tx
                .send(lux)
                .expect("Unable to send new webcam lux value, channel is dead");
        };

        thread::sleep(Duration::from_millis(WAITING_SLEEP_MS));
    }

    fn frame(&mut self) -> Result<(Vec<u8>, usize), Box<dyn Error>> {
        let (device, pixels) = Self::setup(self.video)?;
        let mut stream = Stream::new(&device, Type::VideoCapture)?;
        let (rgbs, _) = stream.next()?;

        Ok((rgbs.to_vec(), pixels))
    }

    fn setup(video: usize) -> Result<(Device, usize), Box<dyn Error>> {
        let device = Device::new(video)?;
        let mut format = device.format()?;
        format.fourcc = FourCC::new(b"RGB3");
        let (width, height) = device
            .enum_framesizes(format.fourcc)?
            .into_iter()
            .flat_map(|f| {
                f.size
                    .to_discrete()
                    .into_iter()
                    .map(|d| (d.width, d.height))
                    .collect_vec()
            })
            .min_by(|&(w1, h1), &(w2, h2)| h1.cmp(&h2).then(w1.cmp(&w2)))
            .ok_or("Unable to find minimum resolution")?;

        format.height = height;
        format.width = width;
        device.set_format(&format)?;

        Ok((device, width as usize * height as usize))
    }
}

pub struct Als {
    webcam_rx: Receiver<u64>,
    thresholds: HashMap<u64, String>,
    lux: RefCell<u64>,
}

impl Als {
    pub fn new(webcam_rx: Receiver<u64>, thresholds: HashMap<u64, String>) -> Self {
        Self {
            webcam_rx,
            thresholds,
            lux: RefCell::new(DEFAULT_LUX),
        }
    }

    fn get_raw(&self) -> Result<u64, Box<dyn Error>> {
        let new_value = self
            .webcam_rx
            .try_iter()
            .last()
            .unwrap_or(*self.lux.borrow());
        *self.lux.borrow_mut() = new_value;
        Ok(new_value)
    }
}

impl super::Als for Als {
    fn get(&self) -> Result<String, Box<dyn Error>> {
        let raw = self.get_raw()?;
        let profile = super::find_profile(raw, &self.thresholds);

        log::trace!("ALS (webcam): {} ({})", profile, raw);
        Ok(profile)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::mpsc;

    fn setup() -> (Als, Sender<u64>) {
        let (webcam_tx, webcam_rx) = mpsc::channel();
        let als = Als::new(webcam_rx, HashMap::default());
        (als, webcam_tx)
    }

    #[test]
    fn test_get_raw_returns_default_value_when_no_data_from_webcam() -> Result<(), Box<dyn Error>> {
        let (als, _) = setup();

        assert_eq!(DEFAULT_LUX, als.get_raw()?);
        Ok(())
    }

    #[test]
    fn test_get_raw_returns_value_from_webcam() -> Result<(), Box<dyn Error>> {
        let (als, webcam_tx) = setup();

        webcam_tx.send(42)?;

        assert_eq!(42, als.get_raw()?);
        Ok(())
    }

    #[test]
    fn test_get_raw_returns_most_recent_value_from_webcam() -> Result<(), Box<dyn Error>> {
        let (als, webcam_tx) = setup();

        webcam_tx.send(42)?;
        webcam_tx.send(43)?;
        webcam_tx.send(44)?;

        assert_eq!(44, als.get_raw()?);
        Ok(())
    }

    #[test]
    fn test_get_raw_returns_last_known_value_from_webcam_when_no_new_data(
    ) -> Result<(), Box<dyn Error>> {
        let (als, webcam_tx) = setup();

        webcam_tx.send(42)?;
        webcam_tx.send(43)?;

        assert_eq!(43, als.get_raw()?);
        assert_eq!(43, als.get_raw()?);
        assert_eq!(43, als.get_raw()?);
        Ok(())
    }
}
