///|
/// Coroutine runtime (minimal, synchronous `await` support).

///|
priv struct CoroutineState {
  body : Array[Stmt]
  locals : Array[(String, Value)]
  globals : Array[(String, Value)]
  builtins : Array[(String, Value)]
  io : MockIO
  global_names : Array[String]
  nonlocal_names : Array[String]
  closure : Array[(String, Value)]
  config : Config
  name : String
  filename : String
  mut done : Bool
}

///|
let coroutine_registry : Ref[Array[Ref[CoroutineState]]] = { val: [], }

///|
let coroutine_active_depth : Ref[Int32] = { val: 0, }

///|
pub fn coroutine_is_active() -> Bool {
  coroutine_active_depth.val > 0
}

///|
fn coroutine_enter_context() -> Unit {
  coroutine_active_depth.val = coroutine_active_depth.val + 1
}

///|
fn coroutine_leave_context() -> Unit {
  coroutine_active_depth.val = coroutine_active_depth.val - 1
}

///|
fn make_coroutine_instance(id : Int32) -> Value {
  Value::Instance(InstanceValue::{
    class: ClassValue::{ name: "coroutine", bases: [], dict: [], },
    dict: [("id", Value::Int(@bigint.BigInt::from_int(id)))],
  })
}

///|
fn coroutine_id_from_instance(
  inst : InstanceValue,
) -> Result[Int32?, RuntimeError] {
  match get_named_value(inst.dict, "id") {
    Some(Value::Int(v)) =>
      match bigint_to_int_checked(v) {
        Ok(i) => Ok(Some(i))
        Err(err) => Err(err)
      }
    Some(Value::Bool(v)) => Ok(Some(if v { 1 } else { 0 }))
    _ => Ok(None)
  }
}

///|
fn coroutine_state_from_value(
  coroutine_value : Value,
) -> Result[Ref[CoroutineState], RuntimeError] {
  match coroutine_value {
    Value::Instance(inst) =>
      if inst.class.name != "coroutine" {
        Err(
          make_runtime_error(
            RuntimeErrorKind::Type,
            "'" + inst.class.name + "' object is not a coroutine",
          ),
        )
      } else {
        let id_opt = match coroutine_id_from_instance(inst) {
          Ok(v) => v
          Err(err) => return Err(err)
        }
        let id = match id_opt {
          Some(id) => id
          None =>
            return Err(
              make_runtime_error(
                RuntimeErrorKind::Runtime,
                "RuntimeError: invalid coroutine".to_string(),
              ),
            )
        }
        if id < 0 || id >= coroutine_registry.val.length() {
          return Err(
            make_runtime_error(
              RuntimeErrorKind::Runtime,
              "RuntimeError: invalid coroutine id".to_string(),
            ),
          )
        }
        Ok(coroutine_registry.val[id])
      }
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "'" +
          type_name_from_value(coroutine_value) +
          "' object is not a coroutine",
        ),
      )
  }
}

///|
fn coroutine_new(
  body : Array[Stmt],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
  global_names : Array[String],
  nonlocal_names : Array[String],
  closure : Array[(String, Value)],
  name : String,
  filename : String,
  config : Config,
) -> Value {
  let state : Ref[CoroutineState] = {
    val: CoroutineState::{
      body,
      locals,
      globals,
      builtins,
      io,
      global_names,
      nonlocal_names,
      closure,
      config,
      name,
      filename,
      done: false,
    },
  }
  let id = coroutine_registry.val.length()
  coroutine_registry.val.push(state)
  make_coroutine_instance(id)
}

///|
pub fn coroutine_await(coroutine_value : Value) -> Result[Value, RuntimeError] {
  let state = match coroutine_state_from_value(coroutine_value) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if state.val.done {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Runtime,
        "RuntimeError: cannot reuse already awaited coroutine".to_string(),
      ),
    )
  }
  let _ = match ensure_recursion_available() {
    Ok(_) => ()
    Err(err) => return Err(err)
  }
  push_traceback_frame(state.val.name, state.val.filename)
  push_closure_env(state.val.closure)
  push_scope_decls_with_nonlocals(
    state.val.global_names,
    state.val.nonlocal_names,
  )
  coroutine_enter_context()
  let result = eval_block(
    state.val.body,
    state.val.locals,
    state.val.globals,
    state.val.builtins,
    state.val.io,
    state.val.config,
  )
  coroutine_leave_context()
  pop_scope_decls()
  pop_closure_env()
  pop_traceback_frame()
  state.val.done = true
  result
}