// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// A JavaScript promise that resolves to a value of type `X`
#external
pub type Promise[X]

///|
#external
priv type JsValue

///|
fn[X] JsValue::make(v : X) -> JsValue = "%identity"

///|
fn[X] JsValue::cast(v : JsValue) -> X = "%identity"

///|
extern "js" fn JsValue::abort_error() -> JsValue =
  #| () => {
  #|   const err = new Error()
  #|   err.name = 'AbortError'
  #|   return err
  #| }

///|
extern "js" fn JsValue::to_string(v : JsValue) -> String =
  #| (v) => v.toString()

///|
/// A JavaScript exception wrapped in MoonBit error
suberror JsError {
  JsError(JsValue)
}

///|
pub impl Show for JsError with fn output(self, logger) {
  let JsError(value) = self
  logger <+ "\{value.to_string()}"
}

///|
/// A JavaScript `AbortController` used to cancel promises
#external
pub type AbortController

///|
/// A JavaScript `AbortSignal` used to cancel promises
#external
pub type AbortSignal

///|
/// Create a new abort controller
pub extern "js" fn AbortController::new() -> AbortController =
  #| () => new AbortController()

///|
/// Get the abort signal of an abort controller.
/// The abort signal will be triggered when `.abort()` is called on the controller.
pub extern "js" fn AbortController::signal(self : Self) -> AbortSignal =
  #| (controller) => controller.signal

///|
/// Abort the abort controller and activate its abort signal.
/// All promises tied to the signal will be cancelled.
pub extern "js" fn AbortController::abort(self : Self) =
  #| (controller) => controller.abort()

///|
extern "js" fn AbortSignal::on_abort(
  signal : AbortSignal,
  f : () -> Unit,
) -> Unit =
  #| (signal, f) => signal.addEventListener('abort', f, { once: true })

///|
extern "js" fn JsValue::then(
  promise : JsValue,
  resolve : (JsValue) -> Unit,
  reject : (JsValue) -> Unit,
) =
  #| (p, resolve, reject) => p.then(resolve, reject)

///|
extern "js" fn JsValue::new_promise(
  f : ((JsValue) -> Unit, (JsValue) -> Unit) -> Unit,
) -> JsValue =
  #| (f) => new Promise(f)

///|
priv struct PromiseWaiter[X] {
  mut coro : @coroutine.Coroutine?
  mut ret : X?
  mut err : Error?
}

///|
/// Wait for a JavaScript promise to resolve and return the fulfilled value,
/// or raise an error if the promise is rejected.
///
/// If `abort_controller` is provided,
/// it should be an abort controller that controls the promise,
/// and `.abort()` will be called on the controller automatically when `wait` is cancelled,
/// cancelling the promise automatically.
///
/// If `abort_controller` is absent, `wait` is non-cancellable.
pub async fn[X] Promise::wait(
  promise : Promise[X],
  abort_controller? : AbortController,
) -> X {
  let waiter : PromiseWaiter[X] = {
    coro: Some(@coroutine.current_coroutine()),
    ret: None,
    err: None,
  }
  fn resolve(value : JsValue) {
    waiter.ret = Some(value.cast())
    if waiter.coro is Some(coro) {
      coro.wake()
      @event_loop.reschedule()
    }
  }

  fn reject(err) {
    waiter.err = Some(JsError(err))
    if waiter.coro is Some(coro) {
      coro.wake()
      @event_loop.reschedule()
    }
  }

  JsValue::make(promise).then(resolve, reject)
  defer {
    waiter.coro = None
  }
  if abort_controller is Some(controller) {
    @coroutine.suspend() catch {
      err => {
        controller.abort()
        raise err
      }
    }
  } else {
    @coroutine.protect_from_cancel(@coroutine.suspend)
  }
  if waiter.err is Some(err) {
    raise err
  } else {
    waiter.ret.unwrap()
  }
}

///|
/// Convenient helper for calling async JavaScript code from MoonBit.
/// `run_promise(f)` create a fresh abort signal, passed it to `f`,
/// and wait for the promise that `f` returns.
/// If `f` resolve to a value, `run_promise` return that value.
/// If `f` is rejected with an error, `run_promise` raise that error.
/// If `run_promise` is cancelled, the abort signal passed to `f` is activated,
/// and the promise returned by `f` should be cancelled automatically.
///
/// `run_promise` should only be used for cancellable JavaScript code
/// (i.e. the promised returned by `f` should properly handle the passed-in abort signal).
/// For non-cancellable JavaScript code, use `Promise::wait()` directly.
pub async fn[X] run_promise(f : (AbortSignal) -> Promise[X]) -> X {
  let controller = AbortController::new()
  let promise = f(controller.signal())
  promise.wait(abort_controller=controller)
}

///|
/// Convert a MoonBit async function into a JavaScript promise.
/// `Promise::from_async(f)` returns a promise that:
///
/// - resolve to the result of `f` when `f` return
/// - reject with the error that `f` raise if `f` fail
///
/// If `abort_signal` is present,
/// `f` will be automatically cancelled when the signal is activated,
/// and the returned promise will reject with JavaScript `AbortError`.
///
/// The async function `f` will be run in a global context,
/// so there is no structured concurrency support for `Promise::from_async`,
/// and this function should only be used for exporting MoonBit code to JavaScript.
///
/// It is undefined whether `f` will actually start running immediately,
/// or start at the next JavaScript event loop.
pub fn[X] Promise::from_async(
  f : async () -> X,
  abort_signal? : AbortSignal,
) -> Promise[X] {
  let promise = JsValue::new_promise((resolve, reject) => {
    let coro = @coroutine.spawn(() => {
      try f() catch {
        @coroutine.Cancelled if @coroutine.is_being_cancelled() =>
          reject(JsValue::abort_error())
        err => reject(JsValue::make(err.to_string()))
      } noraise {
        ret => resolve(JsValue::make(ret))
      }
    })
    if abort_signal is Some(signal) {
      signal.on_abort(() => coro.cancel())
    }
  })
  @event_loop.reschedule()
  promise.cast()
}