// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// BBR (Bottleneck Bandwidth and Round-trip propagation time): the window is the path's
// bandwidth-delay product times a gain that walks through startup, drain, probe-bandwidth
// and probe-RTT. It models the pipe rather than counting acknowledgements, so a loss is not
// its congestion signal — a loss does not halve the window the way NewReno's does, and a
// connection whose bottleneck buffer is shallow keeps its rate. It implements the same
// `Control` contract as NewReno and CUBIC.

///|
/// Where a BBR sender is in its bandwidth-probing cycle.
#derive(Eq, Debug)
pub(all) enum Mode {
  /// Grow the window 2.885× per round until the delivered rate stops rising.
  Startup
  /// Drain the queue startup built, down to the bandwidth-delay product.
  Drain
  /// Walk the gain around 1 to probe for more bandwidth, then yield it.
  ProbeBw
  /// Hold the window at the minimum for one round trip so the path drains and a fresh
  /// round-trip minimum can be sampled.
  ProbeRtt
}

///|
/// The startup gain, `2/ln(2)` (BBR's high gain), rounded.
let startup_gain : Double = 2.885

///|
/// The probe-bandwidth gain for a cycle position: 1.25 to probe, 0.75 to yield, then 1 while
/// the estimate is held. BBR's cycle is eight rounds long, one per position, and the two ends
/// are where the bandwidth estimate is tested (draft-cardwell-iccrg-bbr §4.3).
fn probe_gain(index : Int32) -> Double {
  match index % 8 {
    0 => 1.25
    1 => 0.75
    _ => 1.0
  }
}

///|
/// How much of the bandwidth-delay product the window holds: BBR gives the window twice the
/// model while probing and once while draining, so a queue it built can empty. The pacing gain
/// and the window gain are different numbers on purpose — pacing says how fast to send, the
/// window says how much may be outstanding.
fn cwnd_gain(mode : Mode) -> Double {
  match mode {
    Startup => startup_gain
    Drain => 1.0
    ProbeBw => 2.0
    ProbeRtt => 1.0
  }
}

///|
/// How long BBR holds the window at its floor so the path can drain and a fresh round-trip
/// minimum can be sampled (draft-cardwell-iccrg-bbr §4.4). 200 milliseconds, as BBR uses.
let probe_rtt_period : Int64 = 200_000L

///|
/// How many datagrams the window holds while sampling the round trip. Four packets is BBR's
/// floor for the stay: enough for the peer's acknowledgement to come back, small enough that
/// the queue empties.
let probe_rtt_datagrams : Int64 = 4L

///|
/// A BBR congestion controller, an implementation of `Control`.
pub struct Bbr {
  mut window : Int64
  mut in_flight : Int64
  /// The maximum delivery rate seen, in bytes per microsecond.
  mut btl_bw : Double
  /// The smallest round trip seen, in microseconds; the initial RTT until one arrives.
  mut min_rtt : Int64
  /// When `min_rtt` last improved, for the ten-second probe-RTT trigger.
  mut min_rtt_at : Int64
  mut mode : Mode
  /// Bytes acknowledged in total, the numerator of a delivery-rate sample.
  mut delivered : Int64
  mut sample_at : Int64
  mut sample_delivered : Int64
  /// The bandwidth at the last startup round that still rose, and how many rounds since.
  mut full_bw : Double
  mut full_bw_count : Int32
  mut cycle : Int32
  mut cycle_at : Int64
  mut probe_rtt_at : Int64
  /// Whether a round-trip sample arrived during the probe-RTT stay, which is what lets it end.
  mut probe_rtt_done : Bool
  policy : Policy
}

///|
/// A fresh BBR controller at RFC 9002 §7.2's initial window, in startup.
pub fn Bbr::new(policy~ : Policy = policy) -> Bbr {
  let two = 2L * policy.datagram
  let ten = 10L * policy.datagram
  let floor = if two > 14_720L { two } else { 14_720L }
  let initial = if ten < floor { ten } else { floor }
  {
    window: initial,
    in_flight: 0,
    btl_bw: 0.0,
    min_rtt: policy.initial_rtt,
    min_rtt_at: 0,
    mode: Startup,
    delivered: 0,
    sample_at: 0,
    sample_delivered: 0,
    full_bw: 0.0,
    full_bw_count: 0,
    cycle: 0,
    cycle_at: 0,
    probe_rtt_at: 0,
    probe_rtt_done: false,
    policy,
  }
}

///|
/// The smallest window the controller will set (RFC 9002 §7.2).
pub fn Bbr::minimum(self : Bbr) -> Int64 {
  2L * self.policy.datagram
}

///|
/// Where the controller is in its probing cycle.
pub fn Bbr::mode(self : Bbr) -> Mode {
  self.mode
}

///|
/// The maximum delivery rate seen, in bytes per microsecond.
pub fn Bbr::bandwidth(self : Bbr) -> Double {
  self.btl_bw
}

///|
/// The smallest round trip seen, in microseconds.
pub fn Bbr::min_rtt(self : Bbr) -> Int64 {
  self.min_rtt
}

///|
/// The gain the current mode applies to the bandwidth-delay product when pacing.
fn Bbr::gain(self : Bbr) -> Double {
  match self.mode {
    Startup => startup_gain
    Drain => 1.0 / startup_gain
    ProbeBw => probe_gain(self.cycle)
    ProbeRtt => 1.0
  }
}

///|
/// The bandwidth-delay product in bytes: how much the path can hold in flight at once at the
/// best rate and the smallest round trip seen.
pub fn Bbr::bdp(self : Bbr) -> Int64 {
  (self.btl_bw * self.min_rtt.to_double()).to_int64()
}

///|
/// The window the model asks for: the bandwidth-delay product times the mode's window gain,
/// three datagrams of headroom, floored at the minimum. A loss leaves it unchanged, which is
/// what tells BBR from NewReno. While the round trip is being sampled the window is held at
/// four datagrams whatever the model says, so the queue the last cycle built can drain.
pub fn Bbr::target(self : Bbr) -> Int64 {
  if self.mode == ProbeRtt {
    let floor = self.minimum()
    let window = probe_rtt_datagrams * self.policy.datagram
    return if window > floor { window } else { floor }
  }
  let scaled = (self.bdp().to_double() * cwnd_gain(self.mode)).to_int64()
  let want = scaled + 3L * self.policy.datagram
  let floor = self.minimum()
  if want > floor {
    want
  } else {
    floor
  }
}

///|
/// The defaulted methods of `Control` this controller inherits, named so the promotion to
/// methods on `Bbr` is explicit rather than implied by the impl.
pub extend Bbr with Control::{on_new_path}

///|
pub impl Control for Bbr with fn window(self) {
  self.window
}

///|
pub impl Control for Bbr with fn in_flight(self) {
  self.in_flight
}

///|
pub impl Control for Bbr with fn on_sent(self, size) {
  self.in_flight = self.in_flight + size
}

///|
pub impl Control for Bbr with fn on_ack(self, size) {
  self.in_flight = if self.in_flight > size {
    self.in_flight - size
  } else {
    0L
  }
}

///|
pub impl Control for Bbr with fn on_lost(self, size) {
  self.in_flight = if self.in_flight > size {
    self.in_flight - size
  } else {
    0L
  }
}

///|
/// A congestion signal. BBR does not treat loss as its signal: it leaves the window the
/// model computes alone, and a loss in startup only ends startup early.
pub impl Control for Bbr with fn on_congestion(self, at~, now~) {
  ignore(at)
  ignore(now)
  if self.mode == Startup {
    self.mode = Drain
  }
}

///|
/// Persistent congestion is the one loss that resets the model: the path is not delivering,
/// so the window drops to the minimum and a fresh round-trip minimum is taken.
pub impl Control for Bbr with fn on_persistent(self) {
  self.window = self.minimum()
  self.btl_bw = 0.0
  self.mode = ProbeRtt
  self.probe_rtt_at = 0L
  self.probe_rtt_done = false
}

///|
/// An acknowledgement at a known time: out of flight, into the delivery-rate sample, and the
/// window recomputed from the model.
pub impl Control for Bbr with fn on_ack_at(self, size, now~) {
  self.in_flight = if self.in_flight > size {
    self.in_flight - size
  } else {
    0L
  }
  self.delivered = self.delivered + size
  let elapsed = now - self.sample_at
  if elapsed >= self.min_rtt && elapsed > 0L {
    let rate = (self.delivered - self.sample_delivered).to_double() /
      elapsed.to_double()
    if rate > self.btl_bw {
      self.btl_bw = rate
    }
    self.sample_delivered = self.delivered
    self.sample_at = now
    if self.mode == Startup {
      if self.btl_bw >= self.full_bw * 1.25 {
        self.full_bw = self.btl_bw
        self.full_bw_count = 0
      } else {
        self.full_bw_count = self.full_bw_count + 1
        if self.full_bw_count >= 3 {
          self.mode = Drain
        }
      }
    }
  }
  // ProbeRTT: ten seconds without a fresher round-trip minimum means the path's queue has
  // grown into every sample, so the window drops and waits for a clean one (§4.4).
  if self.btl_bw > 0.0 &&
    self.mode != ProbeRtt &&
    now - self.min_rtt_at > 10_000_000L {
    self.mode = ProbeRtt
    self.probe_rtt_at = now
    self.probe_rtt_done = false
  }
  match self.mode {
    Startup => ()
    Drain =>
      // The queue startup built is gone once what is in flight fits the model.
      if self.in_flight <= self.bdp() {
        self.mode = ProbeBw
        // Enter at a gain of 1: the bandwidth was just measured, so probe from there.
        self.cycle = 2
        self.cycle_at = now
      }
    ProbeBw =>
      // One round per position: the cycle walks 1.25, 0.75, then six rounds at 1.
      if now - self.cycle_at >= self.min_rtt {
        self.cycle = self.cycle + 1
        self.cycle_at = now
      }
    ProbeRtt =>
      // It ends when the stay is over and a sample has arrived; a path that says nothing keeps
      // the window small, which is the safe direction.
      if now - self.probe_rtt_at >= probe_rtt_period && self.probe_rtt_done {
        self.mode = ProbeBw
        self.cycle = 2
        self.cycle_at = now
      }
  }
  self.window = self.target()
}

///|
/// A round-trip sample: keep the smallest, and note when it was taken so probe-RTT knows how
/// long the path has gone without a fresher one.
pub impl Control for Bbr with fn on_rtt(self, sample~, now~) {
  if sample > 0L && sample < self.min_rtt {
    self.min_rtt = sample
    self.min_rtt_at = now
    if self.mode == ProbeRtt {
      // A clean sample during the stay is what it was waiting for; the stay still runs its
      // 200 milliseconds, so the queue has time to empty.
      self.probe_rtt_done = true
    }
  }
}

///|
/// What BBR would pace at: the bottleneck rate times the mode's gain, which is the rate the
/// model has decided the path can carry. The window gain is a different number on purpose —
/// pacing says how fast to send, the window says how much may be outstanding. Nothing is
/// offered before the first delivery-rate sample, so the connection is unpaced until it knows
/// something (RFC 9002 §7.7).
pub impl Control for Bbr with fn pacing_rate(self) {
  if self.btl_bw <= 0.0 {
    None
  } else {
    Some(self.btl_bw * self.gain())
  }
}

///|
pub extend Bbr with Control::{
  window,
  in_flight,
  on_sent,
  on_ack,
  on_ack_at,
  on_lost,
  on_congestion,
  on_persistent,
  on_rtt,
  pacing_rate,
}

///|
pub extend Mode with Debug::{to_repr}

///|
pub extend Mode with Eq::{not_equal, equal}

///|
pub impl Control for Bbr with fn on_new_path(self) {
  let fresh = Bbr::new(policy=self.policy)
  self.window = fresh.window
  self.btl_bw = fresh.btl_bw
  self.min_rtt = fresh.min_rtt
  self.min_rtt_at = fresh.min_rtt_at
  self.mode = fresh.mode
  self.delivered = fresh.delivered
  self.sample_at = fresh.sample_at
  self.sample_delivered = fresh.sample_delivered
  self.full_bw = fresh.full_bw
  self.full_bw_count = fresh.full_bw_count
  self.cycle = fresh.cycle
  self.cycle_at = fresh.cycle_at
  self.probe_rtt_at = fresh.probe_rtt_at
  self.probe_rtt_done = fresh.probe_rtt_done
}