///|
pub(all) struct FilterSample {
  offset_seconds : Double
  delay_seconds : Double
  dispersion_seconds : Double
  received_at : Double
} derive(Debug, Eq)

///|
pub(all) struct FilterEstimate {
  offset_seconds : Double
  delay_seconds : Double
  dispersion_seconds : Double
  jitter_seconds : Double
  selected_at : Double
  evaluated_at : Double
  valid_samples : Int
  reach : Int
  fresh : Bool
} derive(Debug)

///|
pub struct ClockFilter {
  entries : Array[FilterSample]
  precision : Double
  tolerance : Double
  mut last_time : Double?
  mut accepted : FilterEstimate?
  mut reach : Int
} derive(Debug)

///|
/// Eight stages, MAXDISP 16 seconds, PHI 15 ppm; no OS clock discipline.
pub fn ClockFilter::new(
  precision_seconds? : Double = 0.000001,
  frequency_tolerance? : Double = 0.000015,
) -> ClockFilter raise NtpError {
  if !finite(precision_seconds) ||
    precision_seconds <= 0.0 ||
    precision_seconds > 1.0 ||
    !finite(frequency_tolerance) ||
    frequency_tolerance < 0.0 ||
    frequency_tolerance > 0.001 {
    raise Invalid("invalid filter precision or frequency tolerance")
  }
  {
    entries: [],
    precision: precision_seconds,
    tolerance: frequency_tolerance,
    last_time: None,
    accepted: None,
    reach: 0,
  }
}

///|
fn ClockFilter::check_time(
  self : ClockFilter,
  now : Double,
) -> Unit raise NtpError {
  if !finite(now) ||
    now < 0.0 ||
    (self.last_time is Some(previous) && now < previous) {
    raise Invalid("filter requires finite monotonic arrival times")
  }
}

///|
fn ClockFilter::shift(self : ClockFilter, sample : FilterSample) -> Unit {
  self.entries.insert(0, sample)
  if self.entries.length() > 8 {
    ignore(self.entries.pop())
  }
  self.last_time = Some(sample.received_at)
}

///|
/// Call once when beginning a poll. After three missed polls a dummy is shifted
/// into the filter, so eight subsequent missing polls eventually replace it.
pub fn ClockFilter::begin_poll(
  self : ClockFilter,
  now : Double,
) -> Unit raise NtpError {
  self.check_time(now)
  self.reach = (self.reach << 1) & 255
  if (self.reach & 7) == 0 {
    self.shift({
      offset_seconds: 0.0,
      delay_seconds: 16.0,
      dispersion_seconds: 16.0,
      received_at: now,
    })
  } else {
    self.last_time = Some(now)
  }
}

///|
pub fn ClockFilter::observe(
  self : ClockFilter,
  sample : FilterSample,
) -> FilterEstimate? raise NtpError {
  self.check_time(sample.received_at)
  if !finite(sample.offset_seconds) ||
    sample.offset_seconds.abs() > 2147483648.0 ||
    !finite(sample.delay_seconds) ||
    !finite(sample.dispersion_seconds) ||
    sample.delay_seconds < 0.0 ||
    sample.delay_seconds >= 16.0 ||
    sample.dispersion_seconds < 0.0 ||
    sample.dispersion_seconds > 16.0 {
    raise Invalid("invalid or unfit filter sample")
  }
  self.reach = self.reach | 1
  self.shift(sample)
  let result = self.estimate(sample.received_at)
  if result is Some(value) && value.fresh {
    self.accepted = Some(value)
  }
  result
}

///|
/// Snapshot statistics age without mutating stage arrival times. The fresh flag
/// indicates whether this lowest-delay sample has advanced the accepted epoch.
pub fn ClockFilter::estimate(
  self : ClockFilter,
  now : Double,
) -> FilterEstimate? raise NtpError {
  self.check_time(now)
  let sorted = self.entries.copy()
  sorted.sort_by((a, b) => {
    if a.delay_seconds < b.delay_seconds {
      -1
    } else if a.delay_seconds > b.delay_seconds {
      1
    } else if a.received_at > b.received_at {
      -1
    } else if a.received_at < b.received_at {
      1
    } else {
      0
    }
  })
  let valid = sorted.filter(s => s.delay_seconds < 16.0)
  if valid.is_empty() {
    return None
  }
  let best = valid[0]
  let mut dispersion = 0.0
  let mut weight = 0.5
  for i in 0..<8 {
    let amount = if i < sorted.length() {
      let s = sorted[i]
      (s.dispersion_seconds + self.tolerance * (now - s.received_at)).min(16.0)
    } else {
      16.0
    }
    dispersion += amount * weight
    weight *= 0.5
  }
  let mut squares = 0.0
  for s in valid {
    let delta = s.offset_seconds - best.offset_seconds
    squares += delta * delta
  }
  // RMS normalization is inside the square root (RFC 5905 erratum 5600).
  let jitter = if valid.length() > 1 {
    (squares / (valid.length() - 1).to_double()).sqrt().max(self.precision)
  } else {
    self.precision
  }
  let fresh = match self.accepted {
    Some(old) => best.received_at > old.selected_at
    None => true
  }
  let result : FilterEstimate = {
    offset_seconds: best.offset_seconds,
    delay_seconds: best.delay_seconds,
    dispersion_seconds: dispersion,
    jitter_seconds: jitter,
    selected_at: best.received_at,
    evaluated_at: now,
    valid_samples: valid.length(),
    reach: self.reach,
    fresh,
  }
  // Keep only the accepted epoch; old candidates are still returned as a
  // diagnostic snapshot with fresh=false and cannot discipline a clock twice.
  Some(result)
}

///|
pub fn FilterEstimate::root_distance(
  self : FilterEstimate,
  root_delay_seconds : Double,
  root_dispersion_seconds : Double,
) -> Double raise NtpError {
  if !finite(root_delay_seconds) ||
    !finite(root_dispersion_seconds) ||
    root_dispersion_seconds < 0.0 {
    raise Invalid("invalid root metadata")
  }
  (root_delay_seconds + self.delay_seconds).max(0.005) / 2.0 +
  root_dispersion_seconds +
  self.dispersion_seconds +
  self.jitter_seconds
}