// 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 cfg_if::cfg_if;
use core::{
    arch::wasm32,
    sync::atomic::{AtomicI32, Ordering},
};
use instant::Instant;
use std::time::Duration;
use std::{convert::TryFrom, thread};

cfg_if! {
    if #[cfg(all(
        target_arch = "wasm32",
        target_os = "unknown",
        target_vendor = "unknown"
    ))] {
        // This function serves as a polyfill for `Instant::checked_duration_since`, which is
        // currently not implemented for wasm32-unknown-unknown.
        // TODO: Remove this shim once it
        fn checked_duration_since_now(other: Instant) -> Option<Duration> {
            let now = Instant::now();

            if other < now {
                None
            } else {
                Some(other.duration_since(now))
            }
        }
    } else {
        // If we are not targeting wasm32, we can use the native `checked_duration_since`.
        fn checked_duration_since_now(timeout: Instant) -> Option<Duration> {
            timeout.checked_duration_since(Instant::now())
        }
    }
}

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

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

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

    const IS_CHEAP_TO_CONSTRUCT: bool = true;

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

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

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

    #[inline]
    unsafe fn park(&self) {
        while self.parked.load(Ordering::Acquire) == PARKED {
            let r = wasm32::memory_atomic_wait32(self.ptr(), PARKED, -1);
            // we should have either woken up (0) or got a not-equal due to a
            // race (1). We should never time out (2)
            debug_assert!(r == 0 || r == 1);
        }
    }

    #[inline]
    unsafe fn park_until(&self, timeout: Instant) -> bool {
        while self.parked.load(Ordering::Acquire) == PARKED {
            if let Some(left) = checked_duration_since_now(timeout) {
                let nanos_left = i64::try_from(left.as_nanos()).unwrap_or(i64::max_value());
                let r = wasm32::memory_atomic_wait32(self.ptr(), PARKED, nanos_left);
                debug_assert!(r == 0 || r == 1 || r == 2);
            } else {
                return false;
            }
        }
        true
    }

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

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

pub struct UnparkHandle(*mut i32);

impl super::UnparkHandleT for UnparkHandle {
    #[inline]
    unsafe fn unpark(self) {
        let num_notified = wasm32::memory_atomic_notify(self.0 as *mut i32, 1);
        debug_assert!(num_notified == 0 || num_notified == 1);
    }
}

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