///|
/// One unit of scheduled work.
///
/// An event carries a human readable `tag` next to the closure so that a trace
/// of a run can be read (and diffed) without a debugger.
pub(all) struct Scheduled {
  time : Time
  seq : Int
  tag : String
  action : () -> Unit
}

///|
/// Ordering key of the event heap: earlier time first, and for two events at the
/// same instant the one scheduled first runs first.
///
/// This one rule is what makes the whole simulator reproducible. Without the
/// `seq` tie-break, two events scheduled for the same picosecond would run in
/// whatever order the heap implementation happened to produce.
fn earlier(a : Scheduled, b : Scheduled) -> Bool {
  if a.time.ps != b.time.ps {
    a.time.ps < b.time.ps
  } else {
    a.seq < b.seq
  }
}

///|
/// Push onto a binary min-heap.
fn heap_push(heap : Array[Scheduled], item : Scheduled) -> Unit {
  heap.push(item)
  let mut index = heap.length() - 1
  while index > 0 {
    let parent = (index - 1) / 2
    if earlier(heap[index], heap[parent]) {
      heap.swap(index, parent)
      index = parent
    } else {
      break
    }
  }
}

///|
/// The earliest event without removing it.
fn heap_peek(heap : Array[Scheduled]) -> Scheduled? {
  if heap.length() == 0 {
    None
  } else {
    Some(heap[0])
  }
}

///|
/// Remove and return the earliest event.
fn heap_pop(heap : Array[Scheduled]) -> Scheduled? {
  let size = heap.length()
  if size == 0 {
    return None
  }
  let top = heap[0]
  let last = heap[size - 1]
  heap.truncate(size - 1)
  if size > 1 {
    heap[0] = last
    heap_sift_down(heap, 0)
  }
  Some(top)
}

///|
/// Restore the heap property below `start`.
fn heap_sift_down(heap : Array[Scheduled], start : Int) -> Unit {
  let size = heap.length()
  let mut index = start
  while true {
    let left = index * 2 + 1
    if left >= size {
      break
    }
    let right = left + 1
    let mut smallest = left
    if right < size && earlier(heap[right], heap[left]) {
      smallest = right
    }
    if earlier(heap[smallest], heap[index]) {
      heap.swap(index, smallest)
      index = smallest
    } else {
      break
    }
  }
}

///|
/// The deterministic simulation core.
///
/// A simulation is a pure function of its scenario and its seed. `Sim` owns
/// virtual time, the event queue and the random source; nothing reads the wall
/// clock and nothing depends on hash iteration order, so two runs of the same
/// configuration produce byte-identical journals and metrics.
pub struct Sim {
  mut now : Time
  mut seq : Int
  mut processed : Int
  mut recording : Bool
  mut record_events : Bool
  heap : Array[Scheduled]
  rng : Rng
  journal : Array[String]
}

///|
/// Create a simulator whose randomness is derived from `seed`.
pub fn Sim::new(seed : UInt64) -> Sim {
  {
    now: Time::zero(),
    seq: 0,
    processed: 0,
    recording: false,
    record_events: false,
    heap: [],
    rng: Rng::new(seed),
    journal: [],
  }
}

///|
/// The current virtual time.
pub fn Sim::now(self : Sim) -> Time {
  self.now
}

///|
/// The deterministic random source of this run.
pub fn Sim::rng(self : Sim) -> Rng {
  self.rng
}

///|
/// How many events are waiting.
pub fn Sim::pending(self : Sim) -> Int {
  self.heap.length()
}

///|
/// How many events have run so far.
pub fn Sim::processed(self : Sim) -> Int {
  self.processed
}

///|
/// Turn on the event journal. Journalling costs memory, so a long run keeps it
/// off unless the caller asks for a trace to inspect or to diff.
pub fn Sim::start_journal(self : Sim) -> Unit {
  self.recording = true
  self.record_events = true
}

///|
/// Record explicit notes without retaining every scheduled event tag.
///
/// This is useful for protocol traces that need state transitions but not the
/// simulator's lower-level event schedule.
pub fn Sim::start_trace(self : Sim) -> Unit {
  self.recording = true
}

///|
/// Whether the journal is on.
///
/// Callers that would have to build a string to describe an event should ask
/// first. Building it unconditionally makes every run pay for a trace nobody
/// asked for, and on a transfer with tens of thousands of segments that cost
/// dominates everything else.
pub fn Sim::is_recording(self : Sim) -> Bool {
  self.recording
}

///|
/// Append a line to the journal, if journalling is on.
pub fn Sim::note(self : Sim, message : String) -> Unit {
  if self.recording {
    self.journal.push(message)
  }
}

///|
/// The journal collected so far.
pub fn Sim::journal(self : Sim) -> Array[String] {
  self.journal.copy()
}

///|
/// Schedule `action` to run `delay` from now.
pub fn Sim::after(
  self : Sim,
  delay : Time,
  tag : String,
  action : () -> Unit,
) -> Unit {
  self.schedule_at(self.now + delay, tag, action)
}

///|
/// Schedule `action` at an absolute virtual time.
pub fn Sim::at(
  self : Sim,
  time : Time,
  tag : String,
  action : () -> Unit,
) -> Unit {
  self.schedule_at(time, tag, action)
}

///|
/// The single scheduling entry point.
///
/// Scheduling into the past is a bug in the model, not a recoverable condition,
/// so it aborts loudly with the offending tag instead of silently producing a
/// result that looks plausible but cannot happen on a real wire.
pub fn Sim::schedule_at(
  self : Sim,
  time : Time,
  tag : String,
  action : () -> Unit,
) -> Unit {
  if time.ps < self.now.ps {
    abort(
      "event \"" +
      tag +
      "\" scheduled at " +
      time.render() +
      " is in the past; now is " +
      self.now.render(),
    )
  }
  heap_push(self.heap, { time, seq: self.seq, tag, action, })
  self.seq = self.seq + 1
}

///|
/// Run the earliest event. Returns `false` when the queue is empty.
pub fn Sim::step(self : Sim) -> Bool {
  match heap_pop(self.heap) {
    None => false
    Some(event) => {
      self.now = event.time
      self.processed = self.processed + 1
      if self.recording && self.record_events {
        self.journal.push(event.time.render() + "  " + event.tag)
      }
      (event.action)()
      true
    }
  }
}

///|
/// Run every event up to `deadline`, leaving later events queued, and advance
/// the clock to `deadline` so that a run of an empty period still moves time
/// forward the way a real experiment would.
pub fn Sim::run_until(self : Sim, deadline : Time) -> Unit {
  while true {
    match heap_peek(self.heap) {
      None => break
      Some(next) => if next.time.ps > deadline.ps { break }
    }
    self.step() |> ignore
  }
  if deadline.ps > self.now.ps {
    self.now = deadline
  }
}

///|
/// Run for `duration` of virtual time starting now.
pub fn Sim::run_for(self : Sim, duration : Time) -> Unit {
  self.run_until(self.now + duration)
}

///|
/// Drain the queue.
///
/// `max_events` is a safety valve: a model bug that schedules events from inside
/// an event handler could otherwise spin forever, and a hung experiment is worse
/// than a loud failure.
pub fn Sim::run(self : Sim, max_events? : Int = 1_000_000) -> Unit {
  let mut count = 0
  while count < max_events {
    if !self.step() {
      break
    }
    count = count + 1
  }
}

///|
/// Run until `condition` holds, and report whether it ever did.
///
/// Draining the queue tells you the simulation is over; this tells you when the
/// thing you were measuring actually happened. The distinction matters because a
/// protocol leaves superseded timer events behind: they are ignored when they
/// fire, but they are still events, and draining them pushes the clock past the
/// moment of interest. Measuring a transfer by reading the clock after a full
/// drain would report the last stale timer instead of the last byte.
pub fn Sim::step_until(
  self : Sim,
  condition : () -> Bool,
  max_events? : Int = 1_000_000,
) -> Bool {
  let mut count = 0
  while count < max_events {
    if condition() {
      return true
    }
    if !self.step() {
      return condition()
    }
    count = count + 1
  }
  false
}