// The QUIC recovery loop for one packet-number space (RFC 9002): it ties the RTT estimator,
// the sent-packet tracker, and the NewReno congestion controller into the state a sender keeps
// as it puts packets on the wire and processes their acknowledgements. Sending a packet records
// it and charges the congestion window; a received ACK samples the RTT off the largest newly
// acknowledged packet, frees the acknowledged bytes, then runs loss detection — a congestion
// signal halves the window, and the lost packets' numbers come back to be retransmitted. This
// is the pure core the async UDP send loop drives.

///|
/// A sender's recovery state for one packet-number space (RFC 9002).
pub struct QuicRecovery {
  rtt : RttEstimator
  tracker : SentPacketTracker
  cc : NewReno
  max_ack_delay : Int64
  granularity : Int64
  packet_threshold : Int64
  mut pto_count : Int
  mut last_sent_time : Int64
}

///|
fn recovery_pow2(n : Int) -> Int64 {
  let mut r = 1L
  for _i = 0; _i < n; _i = _i + 1 {
    r = r * 2L
  }
  r
}

///|
/// A fresh recovery state: `max_datagram_size` sizes the congestion window, `max_ack_delay`
/// caps a peer's reported ACK delay, and `granularity` floors the timers — all microseconds
/// except the datagram size in bytes. The packet-reordering threshold is kPacketThreshold (3).
pub fn QuicRecovery::new(
  max_datagram_size : Int64,
  max_ack_delay : Int64,
  granularity : Int64,
) -> QuicRecovery {
  {
    rtt: RttEstimator::new(),
    tracker: SentPacketTracker::new(),
    cc: NewReno::new(max_datagram_size),
    max_ack_delay,
    granularity,
    packet_threshold: 3L,
    pto_count: 0,
    last_sent_time: -1L,
  }
}

///|
/// Record an ack-eliciting packet numbered `pn` sent at `now` (microseconds) carrying `size`
/// bytes: track it for acknowledgement and charge the congestion window.
pub fn QuicRecovery::on_packet_sent(
  self : QuicRecovery,
  pn : Int64,
  now : Int64,
  size : Int64,
) -> Unit {
  self.tracker.on_sent(pn, now, size)
  self.cc.on_sent(size)
  self.last_sent_time = now
}

///|
/// Process a received ACK `frame` at `now` with the peer's reported `ack_delay` (RFC 9002
/// §5–§7): sample the RTT off the largest newly acknowledged packet, free the acknowledged
/// bytes in the congestion window, then run loss detection over both thresholds. A non-empty
/// loss is one congestion signal (halving the window) and frees the lost bytes. Returns the
/// packet numbers declared lost — the frames to retransmit. Raises on a malformed ACK frame.
pub fn QuicRecovery::on_ack_received(
  self : QuicRecovery,
  frame : QuicFrame,
  now : Int64,
  ack_delay : Int64,
) -> Array[Int64] raise {
  let ranges = quic_ack_frame_ranges(frame)
  if ranges.length() == 0 {
    return []
  }
  let mut largest = -1L
  for r in ranges {
    if r.1 > largest {
      largest = r.1
    }
  }
  let prev_largest = self.tracker.largest_acked()
  let sample_time = self.tracker.time_of(largest)
  let acked_bytes = self.tracker.on_ack_ranges(ranges)
  self.cc.on_ack(acked_bytes)
  if acked_bytes > 0L {
    self.pto_count = 0
  }
  match sample_time {
    Some(ts) =>
      if largest > prev_largest {
        self.rtt.update(now - ts, ack_delay, self.max_ack_delay)
      }
    None => ()
  }
  let time_threshold = quic_loss_time_threshold(self.rtt, self.granularity)
  let lost = self.tracker.detect_lost_packets(
    now,
    self.packet_threshold,
    time_threshold,
  )
  if lost.length() > 0 {
    for p in lost {
      self.cc.on_packet_lost(p.size)
    }
    self.cc.on_congestion()
  }
  lost.map(p => p.pn)
}

///|
/// Whether `bytes` more may be sent without exceeding the congestion window.
pub fn QuicRecovery::can_send(self : QuicRecovery, bytes : Int64) -> Bool {
  self.cc.can_send(bytes)
}

///|
/// The current congestion window (bytes).
pub fn QuicRecovery::window(self : QuicRecovery) -> Int64 {
  self.cc.window()
}

///|
/// The bytes currently in flight.
pub fn QuicRecovery::in_flight(self : QuicRecovery) -> Int64 {
  self.cc.in_flight()
}

///|
/// The smoothed RTT (microseconds).
pub fn QuicRecovery::smoothed_rtt(self : QuicRecovery) -> Int64 {
  self.rtt.smoothed()
}

///|
/// The probe timeout (microseconds).
pub fn QuicRecovery::pto(self : QuicRecovery) -> Int64 {
  self.rtt.pto(self.max_ack_delay, self.granularity)
}

///|
/// The number of consecutive probe timeouts without an acknowledgement (the backoff exponent).
pub fn QuicRecovery::pto_count(self : QuicRecovery) -> Int {
  self.pto_count
}

///|
/// The time (microseconds) at which the probe timeout fires, armed from the last ack-eliciting
/// packet sent and backed off by `2^pto_count` (RFC 9002 §6.2.1). `None` when nothing is
/// outstanding — the timer is disarmed.
pub fn QuicRecovery::pto_deadline(self : QuicRecovery) -> Int64? {
  if self.tracker.outstanding().length() == 0 {
    None
  } else {
    Some(self.last_sent_time + self.pto() * recovery_pow2(self.pto_count))
  }
}

///|
/// Handle a probe timeout (RFC 9002 §6.2.4): back off the timer for the next arming. The caller
/// sends probe packets — retransmitting outstanding frames or new data.
pub fn QuicRecovery::on_pto(self : QuicRecovery) -> Unit {
  self.pto_count = self.pto_count + 1
}

///|
/// The packet numbers still outstanding.
pub fn QuicRecovery::outstanding(self : QuicRecovery) -> Array[Int64] {
  self.tracker.outstanding()
}