///|
/// Invocation-local cooperative cancellation and elapsed-time deadline.
/// Uses a monotonic clock; time spent suspended or in host code counts.
pub struct InvocationControl {
  deadline : (@perf.Instant, Double)?
  cancel : (() -> Bool)?
  mut cancelled : Bool
  mut timed_out : Bool
  priv mut remaining : Int64?
  exhausted : Ref[Bool]
}

///|
/// A negative timeout is invalid; zero expires before guest execution.
pub fn InvocationControl::new(
  timeout_ms : Int32?,
  cancel : (() -> Bool)?,
  safepoints~ : Int64? = None,
) -> InvocationControl? {
  if (timeout_ms is Some(ms) && ms < 0) ||
    (safepoints is Some(count) && count < 0L) {
    return None
  }
  Some({
    deadline: timeout_ms.map(ms => {
      (@perf.instant_now(), ms.to_double() / 1000.0)
    }),
    cancel,
    cancelled: false,
    timed_out: false,
    remaining: safepoints,
    exhausted: Ref(false),
  })
}

///|
/// Check the deadline without invoking the user's cancellation callback.
pub fn InvocationControl::expired(self : InvocationControl) -> Bool {
  if !self.cancelled &&
    !self.exhausted.val &&
    self.deadline is Some((start, seconds)) {
    if @perf.instant_elapsed_as_secs_f64(start) >= seconds {
      self.timed_out = true
    }
  }
  self.timed_out
}

///|
/// Latch the first observed stop reason for stable result classification.
pub fn InvocationControl::preflight(self : InvocationControl) -> Bool {
  if self.cancelled || self.exhausted.val || self.expired() {
    return true
  }
  if self.cancel is Some(callback) && callback() {
    self.cancelled = true
    return true
  }
  if self.expired() {
    return true
  }
  if self.remaining == Some(0L) {
    self.exhausted.val = true
    return true
  }
  false
}

///|
pub fn InvocationControl::poll(self : InvocationControl) -> Bool {
  if self.preflight() {
    return true
  }
  if self.remaining is Some(count) {
    if count == 0L {
      self.exhausted.val = true
      return true
    }
    self.remaining = Some(count - 1L)
  }
  false
}