use crate::buffer::{EventBuffer, Get, MixedEventBuffer, State};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::Duration;

struct DebouncerThread<B> {
    mutex: Arc<Mutex<B>>,
    thread: JoinHandle<()>,
    stopped: Arc<AtomicBool>,
}

impl<B> DebouncerThread<B> {
    fn new<F>(buffer: B, mut f: F) -> Self
    where
        B: Get + Send + 'static,
        F: FnMut(B::Data) + Send + 'static,
    {
        let mutex = Arc::new(Mutex::new(buffer));
        let stopped = Arc::new(AtomicBool::new(false));
        let thread = thread::spawn({
            let mutex = mutex.clone();
            let stopped = stopped.clone();
            move || {
                while !stopped.load(Ordering::Relaxed) {
                    let state = mutex.lock().unwrap().get();
                    match state {
                        State::Empty => thread::park(),
                        State::Wait(duration) => thread::sleep(duration),
                        State::Ready(data) => f(data),
                    }
                }
            }
        });
        Self {
            mutex,
            thread,
            stopped,
        }
    }

    fn stop(self) -> JoinHandle<()> {
        self.stopped.store(true, Ordering::Relaxed);
        self.thread
    }
}

/// Threaded debouncer wrapping [EventBuffer]. Accepts a common delay and a
/// callback function which is going to be called by a background thread with
/// debounced events.
pub struct EventDebouncer<T>(DebouncerThread<EventBuffer<T>>);

impl<T: PartialEq> EventDebouncer<T> {
    pub fn new<F>(delay: Duration, f: F) -> Self
    where
        F: FnMut(T) + Send + 'static,
        T: Send + 'static,
    {
        Self(DebouncerThread::new(EventBuffer::new(delay), f))
    }

    pub fn put(&self, data: T) {
        self.0.mutex.lock().unwrap().put(data);
        self.0.thread.thread().unpark();
    }

    /// Signals the debouncer thread to quit and returns a
    /// [std::thread::JoinHandle] which can be `.join()`ed in the consumer
    /// thread. The common idiom is: `debouncer.stop().join().unwrap();`
    pub fn stop(self) -> JoinHandle<()> {
        self.0.stop()
    }
}

/// Threaded debouncer wrapping [MixedEventBuffer]. Accepts a callback function
/// which is going to be called by a background thread with debounced events.
/// The delay is specified separately for each event as an argument to
/// [MixedEventBuffer::put()].
pub struct MixedEventDebouncer<T>(DebouncerThread<MixedEventBuffer<T>>);

impl<T: Eq> MixedEventDebouncer<T> {
    pub fn new<F>(f: F) -> Self
    where
        F: FnMut(T) + Send + 'static,
        T: Send + 'static,
    {
        Self(DebouncerThread::new(MixedEventBuffer::new(), f))
    }

    pub fn put(&self, data: T, delay: Duration) {
        self.0.mutex.lock().unwrap().put(data, delay);
        self.0.thread.thread().unpark();
    }

    /// Signals the debouncer thread to quit and returns a
    /// [std::thread::JoinHandle] which can be `.join()`ed in the consumer
    /// thread. The common idiom is: `debouncer.stop().join().unwrap();`
    pub fn stop(self) -> JoinHandle<()> {
        self.0.stop()
    }
}

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

    #[test]
    fn event_debouncer() {
        let (tx, rx) = channel();
        let debouncer = EventDebouncer::new(Duration::from_millis(10), move |s| {
            tx.send(s).unwrap();
        });
        debouncer.put(String::from("Test"));
        debouncer.put(String::from("Test"));
        thread::sleep(Duration::from_millis(20));
        assert!(rx.try_iter().eq([String::from("Test")]));
    }

    #[test]
    fn mixed_event_debouncer() {
        let (tx, rx) = channel();
        let debouncer = MixedEventDebouncer::new(move |s| {
            tx.send(s).unwrap();
        });
        debouncer.put(String::from("Test"), Duration::from_millis(10));
        debouncer.put(String::from("Test"), Duration::from_millis(10));
        thread::sleep(Duration::from_millis(20));
        assert!(rx.try_iter().eq([String::from("Test")]));
    }
}