Documentation
use futures::channel::oneshot;
use js_sys::Function;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
  #[wasm_bindgen(js_name = "setTimeout")]
  pub fn set_timeout(handler: &Function, timeout: u32) -> JsValue;

  #[wasm_bindgen(js_name = "setInterval")]
  pub fn set_interval(handler: &Function, timeout: u32) -> JsValue;

  #[wasm_bindgen(js_name = "clearTimeout")]
  pub fn clear_timeout(handle: JsValue) -> JsValue;

  #[wasm_bindgen(js_name = "clearInterval")]
  pub fn clear_interval(handle: JsValue) -> JsValue;
}

#[must_use]
pub struct Timeout {
  id: Option<JsValue>,
  closure: Option<Closure<dyn FnMut()>>,
}

impl Timeout {
  pub fn new<F>(duration: Duration, callback: F) -> Timeout
  where
    F: FnOnce() + 'static,
  {
    let closure = Closure::once(callback);
    let closure_ref = closure
      .as_ref()
      .unchecked_ref::<js_sys::Function>();

    let duration = duration.as_millis();
    let timeout = u32::try_from(duration).unwrap_or(u32::MAX);
    let id = set_timeout(closure_ref, timeout);

    Timeout { id: Some(id), closure: Some(closure) }
  }

  pub fn forget(mut self) -> JsValue {
    let id = self.id.take().unwrap_throw();
    self.closure.take().unwrap_throw().forget();
    id
  }

  pub fn cancel(mut self) -> Closure<dyn FnMut()> {
    self.closure.take().unwrap_throw()
  }
}

impl Drop for Timeout {
  fn drop(&mut self) {
    if let Some(id) = self.id.take() {
      clear_timeout(id);
    }
  }
}

#[must_use]
pub struct TimeoutFuture {
  _inner: Timeout,
  rx: oneshot::Receiver<()>,
}

impl TimeoutFuture {
  pub fn new(duration: Duration) -> TimeoutFuture {
    let (tx, rx) = oneshot::channel();
    let inner = Timeout::new(duration, move || {
      tx.send(()).unwrap_throw();
    });

    TimeoutFuture { _inner: inner, rx }
  }
}

impl Future for TimeoutFuture {
  type Output = ();

  fn poll(mut self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
    Future::poll(Pin::new(&mut self.rx), ctx).map(|it| it.unwrap_throw())
  }
}

pub async fn sleep(duration: Duration) {
  TimeoutFuture::new(duration).await;
}