///|
/// One lifetime, never reopened. A new operation gets a new lifetime.
pub struct Lifetime {
  priv mut state : State
  priv mut next : Int
}

///|
priv enum State {
  Open(Map[Int, Release])
  Closed
}

///|
/// An idempotent release which removes itself from its parent.
pub struct Release {
  priv mut cleanup : (() -> Unit)?
}

///|
pub fn Lifetime::Lifetime() -> Lifetime {
  { state: Open(Map([])), next: 0, }
}

///|
pub fn Lifetime::is_open(self : Self) -> Bool {
  self.state is Open(_)
}

///|
pub fn Release::close(self : Self) -> Unit {
  let cleanup = self.cleanup
  self.cleanup = None
  if cleanup is Some(cleanup) {
    cleanup()
  }
}

///|
/// Adopt cleanup immediately, even if acquisition completed after close.
/// Cleanup must not throw. Releases run in reverse registration order.
pub fn Lifetime::own(self : Self, cleanup : () -> Unit) -> Release {
  match self.state {
    Closed => {
      cleanup()
      { cleanup: None, }
    }
    Open(releases) => {
      self.next += 1
      while releases.contains(self.next) {
        self.next += 1
      }
      let id = self.next
      let release = {
        cleanup: Some(fn() {
          releases.remove(id)
          cleanup()
        }),
      }
      releases[id] = release
      release
    }
  }
}

///|
pub fn Lifetime::close(self : Self) -> Unit {
  guard self.state is Open(releases) else { return }
  self.state = Closed
  // Map iteration preserves insertion order, including after Int wraparound.
  let pending = releases.values().to_array()
  for i = pending.length() - 1; i >= 0; i = i - 1 {
    pending[i].close()
  }
}

///|
pub fn Lifetime::child(self : Self) -> Lifetime {
  let child = Lifetime()
  let parent_release = self.own(fn() { child.close() })
  ignore(child.own(fn() { parent_release.close() }))
  child
}