// Copyright 2016 Amanieu d'Antras
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

use core::{
    ptr,
    sync::atomic::{AtomicI32, Ordering},
};
use std::{thread, time::Instant};
use syscall::{
    call::futex,
    data::TimeSpec,
    error::{Error, EAGAIN, EFAULT, EINTR, ETIMEDOUT},
    flag::{FUTEX_WAIT, FUTEX_WAKE},
};

const UNPARKED: i32 = 0;
const PARKED: i32 = 1;

// Helper type for putting a thread to sleep until some other thread wakes it up
pub struct ThreadParker {
    futex: AtomicI32,
}

impl super::ThreadParkerT for ThreadParker {
    type UnparkHandle = UnparkHandle;

    const IS_CHEAP_TO_CONSTRUCT: bool = true;

    #[inline]
    fn new() -> ThreadParker {
        ThreadParker {
            futex: AtomicI32::new(UNPARKED),
        }
    }

    #[inline]
    unsafe fn prepare_park(&self) {
        self.futex.store(PARKED, Ordering::Relaxed);
    }

    #[inline]
    unsafe fn timed_out(&self) -> bool {
        self.futex.load(Ordering::Relaxed) != UNPARKED
    }

    #[inline]
    unsafe fn park(&self) {
        while self.futex.load(Ordering::Acquire) != UNPARKED {
            self.futex_wait(None);
        }
    }

    #[inline]
    unsafe fn park_until(&self, timeout: Instant) -> bool {
        while self.futex.load(Ordering::Acquire) != UNPARKED {
            let now = Instant::now();
            if timeout <= now {
                return false;
            }
            let diff = timeout - now;
            if diff.as_secs() > i64::max_value() as u64 {
                // Timeout overflowed, just sleep indefinitely
                self.park();
                return true;
            }
            let ts = TimeSpec {
                tv_sec: diff.as_secs() as i64,
                tv_nsec: diff.subsec_nanos() as i32,
            };
            self.futex_wait(Some(ts));
        }
        true
    }

    #[inline]
    unsafe fn unpark_lock(&self) -> UnparkHandle {
        // We don't need to lock anything, just clear the state
        self.futex.store(UNPARKED, Ordering::Release);

        UnparkHandle { futex: self.ptr() }
    }
}

impl ThreadParker {
    #[inline]
    fn futex_wait(&self, ts: Option<TimeSpec>) {
        let ts_ptr = ts
            .as_ref()
            .map(|ts_ref| ts_ref as *const _)
            .unwrap_or(ptr::null());
        let r = unsafe {
            futex(
                self.ptr(),
                FUTEX_WAIT,
                PARKED,
                ts_ptr as usize,
                ptr::null_mut(),
            )
        };
        match r {
            Ok(r) => debug_assert_eq!(r, 0),
            Err(Error { errno }) => {
                debug_assert!(errno == EINTR || errno == EAGAIN || errno == ETIMEDOUT);
            }
        }
    }

    #[inline]
    fn ptr(&self) -> *mut i32 {
        &self.futex as *const AtomicI32 as *mut i32
    }
}

pub struct UnparkHandle {
    futex: *mut i32,
}

impl super::UnparkHandleT for UnparkHandle {
    #[inline]
    unsafe fn unpark(self) {
        // The thread data may have been freed at this point, but it doesn't
        // matter since the syscall will just return EFAULT in that case.
        let r = futex(self.futex, FUTEX_WAKE, PARKED, 0, ptr::null_mut());
        match r {
            Ok(num_woken) => debug_assert!(num_woken == 0 || num_woken == 1),
            Err(Error { errno }) => debug_assert_eq!(errno, EFAULT),
        }
    }
}

#[inline]
pub fn thread_yield() {
    thread::yield_now();
}
