///|
/// Promise[T] wrapper for JS Promise
// #external
// pub type Promise[T]

///|
/// MoonBit builtin %async.suspend
pub async fn[T, E : Error] suspend(
  f : ((T) -> Unit, (E) -> Unit) -> Unit,
) -> T raise E = "%async.suspend"

///|
/// MoonBit builtin %async.run
pub fn run_async(f : async () -> Unit noraise) -> Unit = "%async.run"

///| Types

///|
extern "js" fn ffi_promise_resolve(value : Any) -> Any =
  #| (value) => Promise.resolve(value)

///|
extern "js" fn ffi_promise_reject(error : Any) -> Any =
  #| (error) => Promise.reject(error)
  #|

///|
extern "js" fn ffi_promise_then(promise : Any, callback : (Any) -> Any) =
  #| (promise, callback) => promise.then(callback)

///|
extern "js" fn ffi_promise_all(promises : Array[Promise[Any]]) -> Promise[Any] =
  #| (promises) => Promise.all(promises)

///|
extern "js" fn ffi_promise_race(promises : Array[Promise[Any]]) -> Promise[Any] =
  #| (promises) => Promise.race(promises)

///|
extern "js" fn ffi_promise_any(promises : Array[Promise[Any]]) -> Promise[Any] =
  #| (promises) => Promise.any(promises)

///|
extern "js" fn ffi_new_promise(
  executor : ((Any) -> Unit, (Any) -> Unit) -> Unit,
) -> Promise[Any] =
  #| (executor) => new Promise(executor)

///|
extern "js" fn ffi_promise_with_resolvers() -> Any =
  #| () => Promise.withResolvers()

///|
/// JavaScript Promise[T]
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
#external
pub type Promise[T]

///|
pub fn[T] Promise::to_any(self : Promise[T]) -> Any = "%identity"

///|
/// JS: new Promise((resolve, reject) => { ... })
/// Create a Promise from an async executor function with resolve/reject callbacks.
/// ```moonbit no-check
/// let p : Promise[Int] = Promise::new(async fn(resolve, _reject) {
///   let result = some_async_operation()
///   resolve(result)
/// })
/// ```
pub fn[A] Promise::new(
  f : async ((A) -> Unit, (Error) -> Unit) -> Unit,
) -> Promise[A] {
  ffi_new_promise(fn(resolve, reject) {
    run_async(async fn() noraise {
      f(a => a |> identity |> resolve, e => e |> identity |> reject) catch {
        e => e |> identity |> reject
      }
    })
  })
  |> identity
}

///|
/// Convert an async function to a Promise.
/// ```moonbit no-check
/// let p : Promise[Int] = from_async(async fn() {
///   let result = some_async_operation()
///   result
/// })
/// ```
pub fn[A] from_async(f : async () -> A) -> Promise[A] noraise {
  let { promise, resolve, reject } = Promise::withResolvers()
  run_async(async fn() noraise {
    try f() |> resolve catch {
      e => reject(e)
    }
  })
  promise
}

///|
/// js: Promise.resolve(value)
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve
pub fn[A] Promise::resolve(x : A) -> Self[A] {
  ffi_promise_resolve(identity(x)) |> identity
}

///|
/// js: Promise.reject(error)
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject
pub fn Promise::reject(x : Any) -> Self[Any] {
  ffi_promise_reject(identity(x)) |> identity
}

///|
pub fn[A, B] Promise::then(
  self : Self[A],
  resolve : (A) -> Self[B] raise,
) -> Promise[B] {
  ffi_promise_then(self.to_any() |> identity, a => {
    (a |> identity |> resolve |> identity) catch {
      e => e |> identity |> ffi_promise_reject |> identity
    }
  })
  |> identity
}

///|
pub fn[A, B] Promise::catch_(
  self : Self[A],
  f : (Error) -> Promise[B],
) -> Promise[B] {
  self.to_any()._call("catch", [any(from_fn1(f))]).cast()
}

///|
/// JS: promise.finally(() => { ... })
pub fn[A] Promise::finally_(self : Self[A], f : () -> Unit) -> Self[A] {
  self.to_any()._call("finally", [any(from_fn0(f))]).cast()
}

///|
/// JS: await promise
/// Waits for the Promise to be resolved or rejected.
pub async fn[T] Promise::wait(self : Self[T]) -> T {
  suspend((ok, err) => {
    self
    .to_any()
    ._call("then", [any(from_fn1(ok))])
    ._call("catch", [any(from_fn1(err))])
    |> ignore
  })
}

///|
/// JS: Promise.all(promises)
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
/// ```moonbit no-check
/// let results = Promise::all([
///   from_async(async fn() { fetch_user() }),
///   from_async(async fn() { fetch_posts() }),
/// ]).wait()
/// ```
pub fn[A] Promise::all(promises : Array[Promise[A]]) -> Promise[Array[A]] {
  ffi_promise_all(promises |> identity) |> identity
}

///|
/// JS: Promise.race(promises)
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
/// ```moonbit no-check
/// let first = Promise::race([
///   from_async(async fn() {
///     sleep(100)
///     "slow"
///   }),
///   from_async(async fn() {
///     sleep(10)
///     "fast"
///   }),
/// ]).wait() // "fast"
/// ```
pub fn[T] Promise::race(promises : Array[Promise[T]]) -> Promise[T] {
  ffi_promise_race(promises |> identity) |> identity
}

///|
/// JS: Promise.any(promises)
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/any
/// ```moonbit no-check
/// let first_success = Promise::any([
///   from_async(async fn() { fetch_from_mirror1() }),
///   from_async(async fn() { fetch_from_mirror2() }),
/// ]).wait()
/// ```
pub fn[T] Promise::any(promises : Array[Promise[T]]) -> Promise[T] {
  ffi_promise_any(promises |> identity) |> identity
}

///|
/// Result type for Promise.allSettled
pub(all) struct SettledResult[T] {
  status : String // "fulfilled" | "rejected"
  value : T?
  reason : Nullable[Any]
}

///|
pub fn[T] SettledResult::to_any(self : SettledResult[T]) -> Any = "%identity"

///|
/// JS: Promise.allSettled(promises)
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled
/// ```moonbit no-check
/// let results = Promise::allSettled([
///   from_async(async fn() { 1 }),
///   from_async(async fn() { fail("error") }),
/// ]).wait()
/// // results[0].status == "fulfilled", results[0].value == Some(1)
/// // results[1].status == "rejected", results[1].reason != null
/// ```
pub fn[A] Promise::allSettled(
  promises : Array[Promise[A]],
) -> Promise[Array[SettledResult[A]]] {
  ffi_promise_all(promises |> identity) |> identity
}

///|
/// JS: Promise.withResolvers()
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/withPromiseResolvers
pub(all) struct PromiseResolvers[T] {
  promise : Promise[T]
  resolve : (T) -> Unit
  reject : (Error) -> Unit
}

///|
/// JS: Promise.withResolvers()
/// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/withPromiseResolvers
pub fn[T] Promise::withResolvers() -> PromiseResolvers[T] {
  ffi_promise_with_resolvers() |> identity
}

///|
/// JS: resolvers.resolve(value)
pub fn[T] PromiseResolvers::resolve(self : Self[T], value : T) -> Unit {
  (self.resolve)(value)
}

///|
/// JS: resolvers.reject(error)
pub fn[T] PromiseResolvers::reject(self : Self[T], error : Error) -> Unit {
  (self.reject)(error)
}

///|
pub fn[R] promisify0(f : async () -> R) -> () -> Promise[R] noraise {
  () => {
    let { promise, resolve, reject } = Promise::withResolvers()
    run_async(async fn() noraise {
      try f() |> resolve catch {
        e => reject(e)
      }
    })
    promise
  }
}

///|
/// Moonbit Async Function to JS Promise Function
pub fn[A, R] promisify1(f : async (A) -> R) -> (A) -> Promise[R] noraise {
  a => {
    let { promise, resolve, reject } = Promise::withResolvers()
    run_async(async fn() noraise {
      try f(a) |> resolve catch {
        e => reject(e)
      }
    })
    promise
  }
}

///|
/// Moonbit Async Function to JS Promise Function
pub fn[A, B, R] promisify2(
  f : async (A, B) -> R,
) -> (A, B) -> Promise[R] noraise {
  (a, b) => {
    let { promise, resolve, reject } = Promise::withResolvers()
    run_async(async fn() noraise {
      try f(a, b) |> resolve catch {
        e => reject(e)
      }
    })
    promise
  }
}

///|
/// Moonbit Async Function to JS Promise Function
pub fn[A, B, C, R] promisify3(
  f : async (A, B, C) -> R,
) -> (A, B, C) -> Promise[R] noraise {
  (a, b, c) => {
    let { promise, resolve, reject } = Promise::withResolvers()
    run_async(async fn() noraise {
      try f(a, b, c) |> resolve catch {
        e => reject(e)
      }
    })
    promise
  }
}

///|
/// Sleep for given milliseconds
extern "js" fn ffi_sleep(ms : Int) -> Promise[Unit] =
  #| (ms) => new Promise((resolve) => setTimeout(resolve, ms))

///|
pub async fn sleep(ms : Int) -> Unit noraise {
  ffi_sleep(ms).wait() catch {
    _ => panic()
  }
}

///|
/// Call async dispose (Symbol.asyncDispose)
/// Usage: defer value |> dispose_async
pub async fn dispose_async(value : Any) -> Unit {
  let dispose_fn : Any = value._get(symbol_async_dispose())
  let promise : Promise[Unit] = Any::_invoke(dispose_fn, []) |> identity
  promise.wait()
}

///|
/// Call asyncDispose but ignore the result
/// now moonbit can not async in defer
/// Usage: defer browser |> dispose_async_ignore
pub fn dispose_async_ignore(value : Any) -> Unit {
  let dispose_fn : Any = value._get(symbol_async_dispose())
  Any::_invoke(dispose_fn, []) |> ignore
}

///|
pub fn dispose(value : Any) -> Unit {
  value._call(symbol_dispose(), []) |> ignore
}

///|
extern "js" fn symbol_dispose() -> String =
  #| () => Symbol.dispose

///|
extern "js" fn symbol_async_dispose() -> String =
  #| () => Symbol.asyncDispose