///|
/// An owned reference to a JavaScript function. The reference belongs to its
/// creating environment; dispose is idempotent and GC is a fallback.
pub type NapiCallback

///|
#external
pub type NapiArguments

///|
extern "c" fn napi_callback_empty() -> NapiCallback = "napi_mbt_callback_empty"

///|
#borrow(callback)
extern "c" fn napi_callback_bind(
  env : NapiEnv,
  value : NapiValue,
  callback : NapiCallback,
) -> Int32 = "napi_mbt_callback_bind"

///|
#borrow(callback, result)
extern "c" fn napi_callback_env_raw(
  callback : NapiCallback,
  result : Ref[NapiEnv],
) -> Int32 = "napi_mbt_callback_env"

///|
#borrow(callback, result)
extern "c" fn napi_callback_call_raw(
  callback : NapiCallback,
  args : NapiValue,
  result : Ref[NapiValue],
) -> Int32 = "napi_mbt_callback_call"

///|
#borrow(callback)
extern "c" fn napi_callback_dispose_raw(callback : NapiCallback) -> Int32 = "napi_mbt_callback_dispose"

///|
pub fn NapiCallback::new(
  env : NapiEnv,
  value : NapiValue,
) -> NapiCallback raise NapiError {
  let callback = napi_callback_empty()
  result_from_status(
    env,
    napi_callback_bind(env, value, callback),
    b"Retaining JavaScript callback",
  )
  callback
}

///|
pub fn NapiCallback::environment(
  self : NapiCallback,
) -> NapiEnv raise NapiError {
  let result = Ref(napi_dummy_env())
  let status = napi_callback_env_raw(self, result)
  // The stored environment may already have closed. Do not call N-API with it.
  if status != 0 {
    raise napi_failure(
      status,
      Error,
      if status == 1006 {
        "ERR_NAPI_DISPOSED"
      } else {
        "ERR_NAPI_ENVIRONMENT"
      },
      "Calling callback outside its owning environment",
    )
  }
  result.val
}

///|
extern "c" fn napi_dummy_env() -> NapiEnv = "napi_mbt_dummy_env"

///|
pub fn NapiCallback::call(
  self : NapiCallback,
  args : NapiValue,
) -> NapiValue raise NapiError {
  let env = self.environment()
  let result = Ref(moonbit_dummy_napi_value())
  result_from_status(
    env,
    napi_callback_call_raw(self, args, result),
    b"Calling JavaScript callback",
  )
  result.val
}

///|
pub fn NapiCallback::dispose(self : NapiCallback) -> Unit raise NapiError {
  let status = napi_callback_dispose_raw(self)
  if status != 0 {
    raise napi_failure(
      status,
      Error,
      "ERR_NAPI_ENVIRONMENT",
      "Disposing JavaScript callback",
    )
  }
}

///|
extern "c" fn napi_arguments_length(args : NapiArguments) -> Int32 = "napi_mbt_arguments_length"

///|
#borrow(result)
extern "c" fn napi_arguments_get_raw(
  env : NapiEnv,
  args : NapiArguments,
  index : Int32,
  result : Ref[NapiValue],
) -> Int32 = "napi_mbt_arguments_get"

///|
pub fn NapiArguments::require(
  self : NapiArguments,
  count : Int32,
) -> Unit raise NapiError {
  if napi_arguments_length(self) < count {
    raise napi_failure(
      1,
      TypeError,
      "ERR_NAPI_ARGUMENTS",
      "Missing callback argument",
    )
  }
}

///|
pub fn NapiArguments::get(
  self : NapiArguments,
  env : NapiEnv,
  index : Int32,
) -> NapiValue raise NapiError {
  let result = Ref(moonbit_dummy_napi_value())
  result_from_status(
    env,
    napi_arguments_get_raw(env, self, index, result),
    b"Reading closure argument",
  )
  result.val
}

///|
#borrow(callback, result)
extern "c" fn napi_closure_new_raw(
  env : NapiEnv,
  callback : (NapiEnv, NapiArguments) -> NapiValue,
  trampoline : FuncRef[
    ((NapiEnv, NapiArguments) -> NapiValue, NapiEnv, NapiArguments) -> NapiValue,
  ],
  result : Ref[NapiValue],
) -> Int32 = "napi_mbt_closure_new"

///|
pub fn napi_closure_new(
  env : NapiEnv,
  callback : (NapiEnv, NapiArguments) -> NapiValue,
) -> NapiValue raise NapiError {
  let result = Ref(moonbit_dummy_napi_value())
  result_from_status(
    env,
    napi_closure_new_raw(
      env,
      callback,
      fn(f, env, args) { f(env, args) },
      result,
    ),
    b"Creating MoonBit closure",
  )
  result.val
}