// Private queue-policy core for at-most-once microtask dispatch.
// It consumes jobs by FIFO position before callback execution, but never
// inspects or invokes the JavaScript callback stored in a Microtask.

///|
priv enum MicrotaskQueuePolicyPhase {
  MicrotaskQueueReady
  MicrotaskQueueSelected
  MicrotaskQueueFailed
  MicrotaskQueueCompleted
}

///|
priv enum MicrotaskQueueReadiness {
  MicrotaskQueueEmpty
  MicrotaskObservationRequired
}

///|
priv struct MicrotaskQueuePolicy {
  queue : Array[Microtask]
  mut next_index : Int
  mut phase : MicrotaskQueuePolicyPhase
}

///|
fn MicrotaskQueuePolicy::MicrotaskQueuePolicy(
  queue : Array[Microtask],
) -> MicrotaskQueuePolicy {
  { queue, next_index: 0, phase: MicrotaskQueueReady }
}

///|
fn microtask_policy_enqueue(queue : Array[Microtask], job : Microtask) -> Unit {
  queue.push(job)
}

///|
fn[T] invalid_microtask_policy_transition(operation : String) -> T raise {
  fail("invalid microtask policy transition: " + operation)
}

///|
fn MicrotaskQueuePolicy::readiness(
  self : MicrotaskQueuePolicy,
) -> MicrotaskQueueReadiness raise {
  guard self.phase is MicrotaskQueueReady else {
    invalid_microtask_policy_transition("readiness requires Ready")
  }
  if self.next_index < self.queue.length() {
    MicrotaskObservationRequired
  } else {
    MicrotaskQueueEmpty
  }
}

///|
fn MicrotaskQueuePolicy::select_next(
  self : MicrotaskQueuePolicy,
) -> Microtask? raise {
  match self.phase {
    MicrotaskQueueReady => {
      guard self.next_index < self.queue.length() else { return None }
      let task = self.queue[self.next_index]
      self.next_index = self.next_index + 1
      self.phase = MicrotaskQueueSelected
      Some(task)
    }
    MicrotaskQueueSelected | MicrotaskQueueFailed | MicrotaskQueueCompleted =>
      invalid_microtask_policy_transition("select_next requires Ready")
  }
}

///|
fn MicrotaskQueuePolicy::complete_selected(
  self : MicrotaskQueuePolicy,
) -> Unit raise {
  guard self.phase is MicrotaskQueueSelected else {
    invalid_microtask_policy_transition("complete_selected requires Selected")
  }
  self.phase = MicrotaskQueueReady
}

///|
fn MicrotaskQueuePolicy::retain_unselected(self : MicrotaskQueuePolicy) -> Unit {
  let remaining = self.queue.length() - self.next_index
  self.queue[self.next_index:].blit_to(self.queue)
  self.queue.truncate(remaining)
}

///|
fn MicrotaskQueuePolicy::reject_dispatch_observation(
  self : MicrotaskQueuePolicy,
) -> Unit raise {
  guard self.phase is MicrotaskQueueReady else {
    invalid_microtask_policy_transition(
      "reject_dispatch_observation requires Ready",
    )
  }
  guard self.next_index < self.queue.length() else {
    invalid_microtask_policy_transition(
      "reject_dispatch_observation requires pending work",
    )
  }
  self.retain_unselected()
  self.phase = MicrotaskQueueFailed
}

///|
fn MicrotaskQueuePolicy::fail_selected(
  self : MicrotaskQueuePolicy,
) -> Unit raise {
  guard self.phase is MicrotaskQueueSelected else {
    invalid_microtask_policy_transition("fail_selected requires Selected")
  }
  self.retain_unselected()
  self.phase = MicrotaskQueueFailed
}

///|
fn MicrotaskQueuePolicy::complete_drain(
  self : MicrotaskQueuePolicy,
) -> Unit raise {
  guard self.phase is MicrotaskQueueReady else {
    invalid_microtask_policy_transition("complete_drain requires Ready")
  }
  guard self.next_index >= self.queue.length() else {
    invalid_microtask_policy_transition(
      "complete_drain requires an exhausted queue",
    )
  }
  self.queue.clear()
  self.phase = MicrotaskQueueCompleted
}