use std::io;
use std::process::Command;
use std::sync::{Arc, Condvar, Mutex};
use std::thread::{Builder, JoinHandle};

#[derive(Debug)]
pub struct Client {
    inner: Arc<Inner>,
}

#[derive(Debug)]
struct Inner {
    count: Mutex<usize>,
    cvar: Condvar,
}

#[derive(Debug)]
pub struct Acquired(());

impl Client {
    pub fn new(limit: usize) -> io::Result<Client> {
        Ok(Client {
            inner: Arc::new(Inner {
                count: Mutex::new(limit),
                cvar: Condvar::new(),
            }),
        })
    }

    pub unsafe fn open(_s: &str) -> Option<Client> {
        None
    }

    pub fn acquire(&self) -> io::Result<Acquired> {
        let mut lock = self.inner.count.lock().unwrap_or_else(|e| e.into_inner());
        while *lock == 0 {
            lock = self
                .inner
                .cvar
                .wait(lock)
                .unwrap_or_else(|e| e.into_inner());
        }
        *lock -= 1;
        Ok(Acquired(()))
    }

    pub fn release(&self, _data: Option<&Acquired>) -> io::Result<()> {
        let mut lock = self.inner.count.lock().unwrap_or_else(|e| e.into_inner());
        *lock += 1;
        drop(lock);
        self.inner.cvar.notify_one();
        Ok(())
    }

    pub fn string_arg(&self) -> String {
        panic!(
            "On this platform there is no cross process jobserver support,
             so Client::configure is not supported."
        );
    }

    pub fn available(&self) -> io::Result<usize> {
        let lock = self.inner.count.lock().unwrap_or_else(|e| e.into_inner());
        Ok(*lock)
    }

    pub fn configure(&self, _cmd: &mut Command) {
        unreachable!();
    }
}

#[derive(Debug)]
pub struct Helper {
    thread: JoinHandle<()>,
}

pub(crate) fn spawn_helper(
    client: crate::Client,
    state: Arc<super::HelperState>,
    mut f: Box<dyn FnMut(io::Result<crate::Acquired>) + Send>,
) -> io::Result<Helper> {
    let thread = Builder::new().spawn(move || {
        state.for_each_request(|_| f(client.acquire()));
    })?;

    Ok(Helper { thread: thread })
}

impl Helper {
    pub fn join(self) {
        // TODO: this is not correct if the thread is blocked in
        // `client.acquire()`.
        drop(self.thread.join());
    }
}
