///|
/// Lifecycle phase of a Store-owned native invocation.
pub(all) enum NativeInvocationPhase {
  Active
  Parked
} derive(Debug, Eq)

///|
/// Invalid transition in the Store-owned native invocation state machine.
pub suberror NativeExecutionStateError {
  ExecutionStateSealed
  UnknownInvocation(Int64)
  InvocationNotActive(Int64)
  InvocationNotParked(Int64)
  InvocationNotCurrent(Int64)
} derive(Debug, Eq)

///|
pub impl Show for NativeExecutionStateError with fn output(self, logger) {
  logger.write_string(@types.compact_show_repr(Repr(self).to_string()))
}

///|
/// One live native invocation.
///
/// `resource_id` identifies the exclusive stack resource assigned to this
/// invocation. The native continuation layer owns the corresponding fiber.
priv struct NativeInvocation {
  id : Int64
  resource_id : Int64
  mut phase : NativeInvocationPhase
  release_fn : () -> Unit
}

///|
/// Store-scoped owner of native invocation lifetimes.
///
/// The active stack models nested native entry. Parked invocations remain in
/// `invocations` but are absent from `active`, so any number of component tasks
/// can retain distinct native resources without sharing an execution stack.
struct NativeExecutionState {
  mut accepting_new_invocations : Bool
  mut next_invocation_id : Int64
  mut next_resource_id : Int64
  active : Array[Int64]
  invocations : Map[Int64, NativeInvocation]
  reusable_resources : Array[Int64]
}

///|
pub fn NativeExecutionState::NativeExecutionState() -> NativeExecutionState {
  {
    accepting_new_invocations: true,
    next_invocation_id: 0L,
    next_resource_id: 0L,
    active: [],
    invocations: Map([]),
    reusable_resources: [],
  }
}

///|
/// Irreversibly reject new invocations while preserving cleanup operations for
/// existing handles.
pub fn NativeExecutionState::seal(self : NativeExecutionState) -> Unit {
  self.accepting_new_invocations = false
}

///|
fn NativeExecutionState::allocate_resource_id(
  self : NativeExecutionState,
) -> Int64 {
  if self.reusable_resources.length() > 0 {
    self.reusable_resources.remove(self.reusable_resources.length() - 1)
  } else {
    let id = self.next_resource_id
    self.next_resource_id = id + 1L
    id
  }
}

///|
pub fn NativeExecutionState::begin(
  self : NativeExecutionState,
  release? : () -> Unit = fn() { () },
) -> NativeInvocationHandle raise NativeExecutionStateError {
  if !self.accepting_new_invocations {
    raise ExecutionStateSealed
  }
  // Retaining the release closure retains everything it captures. Suspendable
  // callers use it to pin code, instances, and native buffers until the
  // invocation either completes or is cancelled.
  let id = self.next_invocation_id
  self.next_invocation_id = id + 1L
  let invocation = {
    id,
    resource_id: self.allocate_resource_id(),
    phase: Active,
    release_fn: release,
  }
  self.invocations.set(id, invocation)
  self.active.push(id)
  NativeInvocationHandle(self, id)
}

///|
fn NativeExecutionState::invocation(
  self : NativeExecutionState,
  id : Int64,
) -> NativeInvocation raise NativeExecutionStateError {
  match self.invocations.get(id) {
    Some(invocation) => invocation
    None => raise UnknownInvocation(id)
  }
}

///|
fn NativeExecutionState::require_current(
  self : NativeExecutionState,
  id : Int64,
) -> NativeInvocation raise NativeExecutionStateError {
  let invocation = self.invocation(id)
  if invocation.phase != Active {
    raise InvocationNotActive(id)
  }
  if self.active.is_empty() || self.active[self.active.length() - 1] != id {
    raise InvocationNotCurrent(id)
  }
  invocation
}

///|
fn NativeExecutionState::release(
  self : NativeExecutionState,
  invocation : NativeInvocation,
) -> Unit {
  self.invocations.remove(invocation.id)
  self.reusable_resources.push(invocation.resource_id)
  (invocation.release_fn)()
}

///|
fn NativeExecutionState::park(
  self : NativeExecutionState,
  id : Int64,
) -> Unit raise NativeExecutionStateError {
  let invocation = self.require_current(id)
  self.active.remove(self.active.length() - 1) |> ignore
  invocation.phase = Parked
}

///|
fn NativeExecutionState::reactivate(
  self : NativeExecutionState,
  id : Int64,
) -> Unit raise NativeExecutionStateError {
  let invocation = self.invocation(id)
  if invocation.phase != Parked {
    raise InvocationNotParked(id)
  }
  invocation.phase = Active
  self.active.push(id)
}

///|
fn NativeExecutionState::complete(
  self : NativeExecutionState,
  id : Int64,
) -> Unit raise NativeExecutionStateError {
  let invocation = self.require_current(id)
  self.active.remove(self.active.length() - 1) |> ignore
  self.release(invocation)
}

///|
fn NativeExecutionState::cancel(
  self : NativeExecutionState,
  id : Int64,
) -> Unit raise NativeExecutionStateError {
  let invocation = self.invocation(id)
  if invocation.phase != Parked {
    raise InvocationNotParked(id)
  }
  self.release(invocation)
}

///|
/// Best-effort scope cleanup after an error.
///
/// Strict user-visible transitions use `complete` and `cancel`; this method is
/// intentionally idempotent so a `defer` can release an unfinished invocation
/// without masking the original trap.
pub fn NativeExecutionState::release_if_live(
  self : NativeExecutionState,
  id : Int64,
) -> Unit {
  guard self.invocations.get(id) is Some(invocation) else { return }
  match invocation.phase {
    Active =>
      if self.active.length() > 0 && self.active[self.active.length() - 1] == id {
        self.active.remove(self.active.length() - 1) |> ignore
        self.release(invocation)
      }
    Parked => self.release(invocation)
  }
}

///|
pub fn NativeExecutionState::current_invocation(
  self : NativeExecutionState,
) -> Int64? {
  if self.active.is_empty() {
    None
  } else {
    Some(self.active[self.active.length() - 1])
  }
}

///|
pub fn NativeExecutionState::active_depth(self : NativeExecutionState) -> Int {
  self.active.length()
}

///|
pub fn NativeExecutionState::parked_count(self : NativeExecutionState) -> Int {
  let mut count = 0
  for entry in self.invocations.iter() {
    if entry.1.phase == Parked {
      count = count + 1
    }
  }
  count
}

///|
pub fn NativeExecutionState::live_count(self : NativeExecutionState) -> Int {
  self.invocations.length()
}

///|
pub impl Debug for NativeExecutionState with fn to_repr(self) {
  Repr::ctor("NativeExecutionState", [
    (Some("active_depth"), Repr(self.active_depth())),
    (Some("parked_count"), Repr(self.parked_count())),
    (Some("live_count"), Repr(self.live_count())),
  ])
}

///|
/// Stable handle for one Store-owned native invocation.
struct NativeInvocationHandle(NativeExecutionState, Int64)

///|
pub fn NativeInvocationHandle::id(self : NativeInvocationHandle) -> Int64 {
  self.1
}

///|
pub fn NativeInvocationHandle::resource_id(
  self : NativeInvocationHandle,
) -> Int64 raise NativeExecutionStateError {
  self.0.invocation(self.1).resource_id
}

///|
pub fn NativeInvocationHandle::phase(
  self : NativeInvocationHandle,
) -> NativeInvocationPhase raise NativeExecutionStateError {
  self.0.invocation(self.1).phase
}

///|
pub fn NativeInvocationHandle::park(
  self : NativeInvocationHandle,
) -> Unit raise NativeExecutionStateError {
  self.0.park(self.1)
}

///|
pub fn NativeInvocationHandle::reactivate(
  self : NativeInvocationHandle,
) -> Unit raise NativeExecutionStateError {
  self.0.reactivate(self.1)
}

///|
pub fn NativeInvocationHandle::complete(
  self : NativeInvocationHandle,
) -> Unit raise NativeExecutionStateError {
  self.0.complete(self.1)
}

///|
pub fn NativeInvocationHandle::cancel(
  self : NativeInvocationHandle,
) -> Unit raise NativeExecutionStateError {
  self.0.cancel(self.1)
}

///|
pub fn NativeInvocationHandle::release_if_live(
  self : NativeInvocationHandle,
) -> Unit {
  self.0.release_if_live(self.1)
}

///|
pub impl Debug for NativeInvocationHandle with fn to_repr(self) {
  Repr::ctor("NativeInvocationHandle", [(Some("id"), Repr(self.1))])
}