///|
fn ensure_resource_runtime_open(runtime : Runtime) -> Result[Unit, V8Error] {
  if runtime.is_closed() {
    Err(V8Error::RuntimeClosed)
  } else {
    Ok(())
  }
}

///|
fn close_resource_entry(entry : RuntimeResourceEntry) -> Unit {
  match entry.on_close {
    Some(on_close) => on_close()
    None => ()
  }
}

///|
fn runtime_take_resource_entry(
  runtime : Runtime,
  rid : Int,
) -> RuntimeResourceEntry? {
  let entry = runtime.state.val.resources.entries.get(rid)
  match entry {
    Some(entry) => {
      runtime.state.val.resources.entries.remove(rid)
      Some(entry)
    }
    None => None
  }
}

///|
fn runtime_add_resource_entry(
  runtime : Runtime,
  name : String,
  on_close : (() -> Unit)?,
  is_ref : Bool,
) -> Result[Int, V8Error] {
  let open = ensure_resource_runtime_open(runtime)
  guard open is Ok(_) else { return Err(open.unwrap_err()) }
  let rid = runtime.state.val.resources.next_rid
  runtime.state.val.resources.next_rid += 1
  runtime.state.val.resources.entries.set(rid, RuntimeResourceEntry::{
    name,
    on_close,
    is_ref,
  })
  Ok(rid)
}

///|
fn runtime_resource_exists(
  runtime : Runtime,
  rid : Int,
) -> Result[Bool, V8Error] {
  let open = ensure_resource_runtime_open(runtime)
  guard open is Ok(_) else { return Err(open.unwrap_err()) }
  Ok(runtime.state.val.resources.entries.contains(rid))
}

///|
fn runtime_set_resource_ref_state(
  runtime : Runtime,
  rid : Int,
  is_ref : Bool,
) -> Result[Bool, V8Error] {
  let open = ensure_resource_runtime_open(runtime)
  guard open is Ok(_) else { return Err(open.unwrap_err()) }
  let mut changed = false
  runtime.state.val.resources.entries.update(rid, entry => {
    match entry {
      Some(entry) => {
        changed = true
        Some(RuntimeResourceEntry::{
          name: entry.name,
          on_close: entry.on_close,
          is_ref,
        })
      }
      None => None
    }
  })
  Ok(changed)
}

///|
fn runtime_resource_list_json(runtime : Runtime) -> Result[String, V8Error] {
  let listed = runtime.list_resources()
  guard listed is Ok(_) else { return Err(listed.unwrap_err()) }
  let values : Map[String, Json] = Map::new()
  listed
  .unwrap()
  .each(resource => {
    values.set(resource.rid.to_string(), Json::string(resource.name))
  })
  Ok(Json::object(values).stringify())
}

///|
fn parse_resource_rid(payload_json : String) -> Result[Int, String] {
  try @strconv.parse_int(payload_json.trim()) |> Ok catch {
    StrConvError(message) =>
      Err("invalid resource id payload: " + payload_json + " (" + message + ")")
  }
}

///|
fn runtime_close_all_resources(runtime : Runtime) -> Unit {
  let entries : Array[RuntimeResourceEntry] = []
  runtime.state.val.resources.entries.each((_rid, entry) => entries.push(entry))
  runtime.state.val.resources.entries.clear()
  entries.each(entry => close_resource_entry(entry))
}

///|
pub fn Runtime::add_resource(
  self : Runtime,
  name : String,
) -> Result[Int, V8Error] {
  runtime_add_resource_entry(self, name, None, true)
}

///|
pub fn Runtime::add_resource_with_close(
  self : Runtime,
  name : String,
  on_close : () -> Unit,
) -> Result[Int, V8Error] {
  runtime_add_resource_entry(self, name, Some(on_close), true)
}

///|
pub fn Runtime::ref_resource(
  self : Runtime,
  rid : Int,
) -> Result[Bool, V8Error] {
  runtime_set_resource_ref_state(self, rid, true)
}

///|
pub fn Runtime::unref_resource(
  self : Runtime,
  rid : Int,
) -> Result[Bool, V8Error] {
  runtime_set_resource_ref_state(self, rid, false)
}

///|
pub fn Runtime::get_resource_name(
  self : Runtime,
  rid : Int,
) -> Result[String?, V8Error] {
  let open = ensure_resource_runtime_open(self)
  guard open is Ok(_) else { return Err(open.unwrap_err()) }
  Ok(self.state.val.resources.entries.get(rid).map(entry => entry.name))
}

///|
pub fn Runtime::list_resources(
  self : Runtime,
) -> Result[Array[RuntimeResource], V8Error] {
  let open = ensure_resource_runtime_open(self)
  guard open is Ok(_) else { return Err(open.unwrap_err()) }
  let resources : Array[RuntimeResource] = []
  self.state.val.resources.entries.each((rid, entry) => {
    resources.push(RuntimeResource::{
      rid,
      name: entry.name,
      is_ref: entry.is_ref,
    })
  })
  resources.sort_by((left, right) => left.rid - right.rid)
  Ok(resources)
}

///|
pub fn Runtime::try_close_resource(
  self : Runtime,
  rid : Int,
) -> Result[Bool, V8Error] {
  let open = ensure_resource_runtime_open(self)
  guard open is Ok(_) else { return Err(open.unwrap_err()) }
  match runtime_take_resource_entry(self, rid) {
    Some(entry) => {
      close_resource_entry(entry)
      Ok(true)
    }
    None => Ok(false)
  }
}

///|
pub fn Runtime::close_resource(
  self : Runtime,
  rid : Int,
) -> Result[Unit, V8Error] {
  let closed = self.try_close_resource(rid)
  guard closed is Ok(_) else { return Err(closed.unwrap_err()) }
  if closed.unwrap() {
    Ok(())
  } else {
    Err(V8Error::NativeError("resource not found: " + rid.to_string()))
  }
}