///|
pub enum PromiseState {
  Pending
  Fulfilled
  Rejected
} derive(Eq, Show)

///|
struct PromiseHandleState {
  runtime : Ref[RuntimeState]
  mut handle : UInt64
}

///|
pub struct PromiseHandle {
  priv state : Ref[PromiseHandleState]
}

///|
fn promise_handle_from_runtime(
  runtime : Runtime,
  handle : UInt64,
) -> PromiseHandle {
  PromiseHandle::{
    state: Ref::new(PromiseHandleState::{ runtime: runtime.state, handle }),
  }
}

///|
fn promise_state_from_code(code : Int) -> Result[PromiseState, V8Error] {
  match code {
    0 => Ok(PromiseState::Pending)
    1 => Ok(PromiseState::Fulfilled)
    2 => Ok(PromiseState::Rejected)
    _ => Err(V8Error::NativeError("unknown promise state code"))
  }
}

///|
fn promise_runtime(handle : PromiseHandle) -> Runtime {
  Runtime::{ state: handle.state.val.runtime }
}

///|
fn wait_for_promise_handle(
  handle : PromiseHandle,
  remaining : Int,
) -> Result[Unit, V8Error] {
  let state = handle.state()
  guard state is Ok(_) else { return Err(state.unwrap_err()) }
  match state.unwrap() {
    PromiseState::Pending =>
      if remaining <= 0 {
        Err(
          V8Error::NativeError(
            "promise is still pending after 1024 microtask checkpoints",
          ),
        )
      } else {
        let flushed = promise_runtime(handle).perform_microtask_checkpoint()
        guard flushed is Ok(_) else { return Err(flushed.unwrap_err()) }
        wait_for_promise_handle(handle, remaining - 1)
      }
    _ => Ok(())
  }
}

///|
pub fn Runtime::eval_promise_string(
  self : Runtime,
  source : String,
) -> Result[PromiseHandle, V8Error] {
  let handle = self.state.val.handle
  if handle == 0 {
    return Err(V8Error::RuntimeClosed)
  }
  let err_ptr = make_ptr_buffer()
  native_ptr_clear(err_ptr)
  let source_bytes = @utf8.encode(source)
  let promise_handle = runtime_eval_promise_native(
    handle,
    source_bytes,
    source_bytes.length(),
    err_ptr,
  )
  if promise_handle == 0 {
    Err(
      take_native_error(
        err_ptr,
        V8Error::NativeError("runtime_eval_promise_native failed"),
      ),
    )
  } else if not(native_ptr_is_null(err_ptr)) {
    Err(
      take_native_error(
        err_ptr,
        V8Error::NativeError("runtime_eval_promise_native failed"),
      ),
    )
  } else {
    Ok(promise_handle_from_runtime(self, promise_handle))
  }
}

///|
pub fn Runtime::eval_promise_string_with_name(
  self : Runtime,
  resource_name : String,
  source : String,
) -> Result[PromiseHandle, V8Error] {
  let handle = self.state.val.handle
  if handle == 0 {
    return Err(V8Error::RuntimeClosed)
  }
  let err_ptr = make_ptr_buffer()
  native_ptr_clear(err_ptr)
  let resource_name_bytes = @utf8.encode(resource_name)
  let source_bytes = @utf8.encode(source)
  let promise_handle = runtime_eval_promise_with_name_native(
    handle,
    resource_name_bytes,
    resource_name_bytes.length(),
    source_bytes,
    source_bytes.length(),
    err_ptr,
  )
  if promise_handle == 0 {
    Err(
      take_native_error(
        err_ptr,
        V8Error::NativeError("runtime_eval_promise_with_name_native failed"),
      ),
    )
  } else if not(native_ptr_is_null(err_ptr)) {
    Err(
      take_native_error(
        err_ptr,
        V8Error::NativeError("runtime_eval_promise_with_name_native failed"),
      ),
    )
  } else {
    Ok(promise_handle_from_runtime(self, promise_handle))
  }
}

///|
pub async fn Runtime::eval_promise_string_async(
  self : Runtime,
  source : String,
) -> Result[String, V8Error] {
  let promise = self.eval_promise_string(source)
  guard promise is Ok(_) else { return Err(promise.unwrap_err()) }
  promise.unwrap().await_string_async()
}

///|
pub async fn Runtime::eval_promise_string_async_with_name(
  self : Runtime,
  resource_name : String,
  source : String,
) -> Result[String, V8Error] {
  let promise = self.eval_promise_string_with_name(resource_name, source)
  guard promise is Ok(_) else { return Err(promise.unwrap_err()) }
  promise.unwrap().await_string_async()
}

///|
pub async fn Runtime::eval_promise_json_async(
  self : Runtime,
  source : String,
) -> Result[String, V8Error] {
  let promise = self.eval_promise_string(source)
  guard promise is Ok(_) else { return Err(promise.unwrap_err()) }
  promise.unwrap().await_json_async()
}

///|
pub async fn Runtime::eval_promise_json_async_with_name(
  self : Runtime,
  resource_name : String,
  source : String,
) -> Result[String, V8Error] {
  let promise = self.eval_promise_string_with_name(resource_name, source)
  guard promise is Ok(_) else { return Err(promise.unwrap_err()) }
  promise.unwrap().await_json_async()
}

///|
pub async fn Runtime::eval_promise_bytes_async(
  self : Runtime,
  source : String,
) -> Result[Bytes, V8Error] {
  let promise = self.eval_promise_string(source)
  guard promise is Ok(_) else { return Err(promise.unwrap_err()) }
  promise.unwrap().await_bytes_async()
}

///|
pub async fn Runtime::eval_promise_bytes_async_with_name(
  self : Runtime,
  resource_name : String,
  source : String,
) -> Result[Bytes, V8Error] {
  let promise = self.eval_promise_string_with_name(resource_name, source)
  guard promise is Ok(_) else { return Err(promise.unwrap_err()) }
  promise.unwrap().await_bytes_async()
}

///|
pub fn PromiseHandle::is_closed(self : PromiseHandle) -> Bool {
  self.state.val.handle == 0 || self.state.val.runtime.val.handle == 0
}

///|
pub fn PromiseHandle::dispose(self : PromiseHandle) -> Unit {
  let runtime_handle = self.state.val.runtime.val.handle
  let promise_handle = self.state.val.handle
  if runtime_handle != 0 && promise_handle != 0 {
    runtime_promise_delete_native(runtime_handle, promise_handle)
  }
  self.state.val.handle = 0
}

///|
pub fn PromiseHandle::state(
  self : PromiseHandle,
) -> Result[PromiseState, V8Error] {
  let runtime_handle = self.state.val.runtime.val.handle
  if runtime_handle == 0 {
    return Err(V8Error::RuntimeClosed)
  }
  let promise_handle = self.state.val.handle
  if promise_handle == 0 {
    return Err(V8Error::PromiseClosed)
  }
  let err_ptr = make_ptr_buffer()
  native_ptr_clear(err_ptr)
  let state_code = runtime_promise_state_native(
    runtime_handle, promise_handle, err_ptr,
  )
  if not(native_ptr_is_null(err_ptr)) {
    Err(
      take_native_error(
        err_ptr,
        V8Error::NativeError("runtime_promise_state_native failed"),
      ),
    )
  } else {
    promise_state_from_code(state_code)
  }
}

///|
pub fn PromiseHandle::result_string(
  self : PromiseHandle,
) -> Result[String, V8Error] {
  let runtime_handle = self.state.val.runtime.val.handle
  if runtime_handle == 0 {
    return Err(V8Error::RuntimeClosed)
  }
  let promise_handle = self.state.val.handle
  if promise_handle == 0 {
    return Err(V8Error::PromiseClosed)
  }
  let err_ptr = make_ptr_buffer()
  native_ptr_clear(err_ptr)
  let result_bytes = runtime_promise_result_native(
    runtime_handle, promise_handle, err_ptr,
  )
  if not(native_ptr_is_null(err_ptr)) {
    Err(
      take_native_error(
        err_ptr,
        V8Error::NativeError("runtime_promise_result_native failed"),
      ),
    )
  } else {
    Ok(@utf8.decode_lossy(result_bytes))
  }
}

///|
pub fn PromiseHandle::result_json(
  self : PromiseHandle,
) -> Result[String, V8Error] {
  let runtime_handle = self.state.val.runtime.val.handle
  if runtime_handle == 0 {
    return Err(V8Error::RuntimeClosed)
  }
  let promise_handle = self.state.val.handle
  if promise_handle == 0 {
    return Err(V8Error::PromiseClosed)
  }
  let err_ptr = make_ptr_buffer()
  native_ptr_clear(err_ptr)
  let result_bytes = runtime_promise_result_json_native(
    runtime_handle, promise_handle, err_ptr,
  )
  if not(native_ptr_is_null(err_ptr)) {
    Err(
      take_native_error(
        err_ptr,
        V8Error::NativeError("runtime_promise_result_json_native failed"),
      ),
    )
  } else {
    Ok(@utf8.decode_lossy(result_bytes))
  }
}

///|
pub fn PromiseHandle::result_bytes(
  self : PromiseHandle,
) -> Result[Bytes, V8Error] {
  let runtime_handle = self.state.val.runtime.val.handle
  if runtime_handle == 0 {
    return Err(V8Error::RuntimeClosed)
  }
  let promise_handle = self.state.val.handle
  if promise_handle == 0 {
    return Err(V8Error::PromiseClosed)
  }
  let err_ptr = make_ptr_buffer()
  native_ptr_clear(err_ptr)
  let result_bytes = runtime_promise_result_bytes_native(
    runtime_handle, promise_handle, err_ptr,
  )
  if not(native_ptr_is_null(err_ptr)) {
    Err(
      take_native_error(
        err_ptr,
        V8Error::NativeError("runtime_promise_result_bytes_native failed"),
      ),
    )
  } else {
    Ok(result_bytes)
  }
}

///|
pub fn PromiseHandle::await_string(
  self : PromiseHandle,
) -> Result[String, V8Error] {
  let waited = wait_for_promise_handle(self, 1024)
  guard waited is Ok(_) else { return Err(waited.unwrap_err()) }
  self.result_string()
}

///|
pub async fn PromiseHandle::await_string_async(
  self : PromiseHandle,
) -> Result[String, V8Error] {
  let waited = wait_for_promise_handle_async(self)
  guard waited is Ok(_) else { return Err(waited.unwrap_err()) }
  self.result_string()
}

///|
pub fn PromiseHandle::await_json(
  self : PromiseHandle,
) -> Result[String, V8Error] {
  let waited = wait_for_promise_handle(self, 1024)
  guard waited is Ok(_) else { return Err(waited.unwrap_err()) }
  self.result_json()
}

///|
pub async fn PromiseHandle::await_json_async(
  self : PromiseHandle,
) -> Result[String, V8Error] {
  let waited = wait_for_promise_handle_async(self)
  guard waited is Ok(_) else { return Err(waited.unwrap_err()) }
  self.result_json()
}

///|
pub fn PromiseHandle::await_bytes(
  self : PromiseHandle,
) -> Result[Bytes, V8Error] {
  let waited = wait_for_promise_handle(self, 1024)
  guard waited is Ok(_) else { return Err(waited.unwrap_err()) }
  self.result_bytes()
}

///|
pub async fn PromiseHandle::await_bytes_async(
  self : PromiseHandle,
) -> Result[Bytes, V8Error] {
  let waited = wait_for_promise_handle_async(self)
  guard waited is Ok(_) else { return Err(waited.unwrap_err()) }
  self.result_bytes()
}

///|
pub async fn eval_promise_string_async(
  source : String,
) -> Result[String, V8Error] {
  with_runtime_async(runtime => runtime.eval_promise_string_async(source))
}

///|
pub async fn eval_promise_string_async_with_name(
  resource_name : String,
  source : String,
) -> Result[String, V8Error] {
  with_runtime_async(runtime => {
    runtime.eval_promise_string_async_with_name(resource_name, source)
  })
}

///|
pub async fn eval_promise_json_async(
  source : String,
) -> Result[String, V8Error] {
  with_runtime_async(runtime => runtime.eval_promise_json_async(source))
}

///|
pub async fn eval_promise_json_async_with_name(
  resource_name : String,
  source : String,
) -> Result[String, V8Error] {
  with_runtime_async(runtime => {
    runtime.eval_promise_json_async_with_name(resource_name, source)
  })
}

///|
pub async fn eval_promise_bytes_async(
  source : String,
) -> Result[Bytes, V8Error] {
  with_runtime_async(runtime => runtime.eval_promise_bytes_async(source))
}

///|
pub async fn eval_promise_bytes_async_with_name(
  resource_name : String,
  source : String,
) -> Result[Bytes, V8Error] {
  with_runtime_async(runtime => {
    runtime.eval_promise_bytes_async_with_name(resource_name, source)
  })
}