///|
/// Generic weighted counters for stream dashboards.
pub struct OnlineCounter {
  values : Map[String, Double]
  mut updates : Int
}

///|
pub fn OnlineCounter::new() -> OnlineCounter {
  { values: {}, updates: 0 }
}

///|
pub fn OnlineCounter::add(
  self : OnlineCounter,
  key : String,
  value? : Double = 1.0,
) -> Unit {
  self.values.update_or_default(key, 0.0, previous => previous + value)
  self.updates += 1
}

///|
pub fn OnlineCounter::get(self : OnlineCounter, key : String) -> Double {
  self.values.get(key).unwrap_or(0.0)
}

///|
pub fn OnlineCounter::keys(self : OnlineCounter) -> Array[String] {
  self.values.keys().to_array()
}

///|
pub fn OnlineCounter::total(self : OnlineCounter) -> Double {
  self.values.values().fold(init=0.0, (total, value) => total + value)
}

///|
pub fn OnlineCounter::updates(self : OnlineCounter) -> Int {
  self.updates
}

///|
pub fn OnlineCounter::reset(self : OnlineCounter) -> Unit {
  self.values.clear()
  self.updates = 0
}

///|
pub struct WeightedHistogram {
  lower : Double
  upper : Double
  bins : Array[Double]
  mut total : Double
}

///|
pub fn WeightedHistogram::new(
  lower? : Double = 0.0,
  upper? : Double = 1.0,
  bins? : Int = 20,
) -> WeightedHistogram {
  let count = if bins < 1 { 1 } else { bins }
  {
    lower,
    upper: if upper <= lower {
      lower + 1.0
    } else {
      upper
    },
    bins: Array::make(count, 0.0),
    total: 0.0,
  }
}

///|
pub fn WeightedHistogram::observe(
  self : WeightedHistogram,
  value : Double,
  weight? : Double = 1.0,
) -> Unit {
  let ratio = (value - self.lower) / (self.upper - self.lower)
  let index = if ratio <= 0.0 {
    0
  } else if ratio >= 1.0 {
    self.bins.length() - 1
  } else {
    (ratio * self.bins.length().to_double()).to_int()
  }
  self.bins[index] += weight
  self.total += weight
}

///|
pub fn WeightedHistogram::probabilities(
  self : WeightedHistogram,
) -> Array[Double] {
  if self.total <= 0.0 {
    Array::make(self.bins.length(), 0.0)
  } else {
    scale_values(self.bins, 1.0 / self.total)
  }
}

///|
pub fn WeightedHistogram::quantile(
  self : WeightedHistogram,
  probability : Double,
) -> Double {
  let target = clamp(probability, 0.0, 1.0) * self.total
  let mut cumulative = 0.0
  let mut index = self.bins.length() - 1
  for i in 0..= target && index == self.bins.length() - 1 {
      index = i
    }
  }
  self.lower +
  (index.to_double() + 0.5) *
  (self.upper - self.lower) /
  self.bins.length().to_double()
}

///|
pub fn WeightedHistogram::total(self : WeightedHistogram) -> Double {
  self.total
}

///|
pub fn WeightedHistogram::counts(self : WeightedHistogram) -> Array[Double] {
  copy_vector(self.bins)
}

///|
pub fn WeightedHistogram::reset(self : WeightedHistogram) -> Unit {
  self.bins.fill(0.0)
  self.total = 0.0
}

///|
pub struct RateLimiter {
  limit : Int
  period : Int
  mut ticks : Int
  mut accepted : Int
}

///|
pub fn RateLimiter::new(limit : Int, period : Int) -> RateLimiter {
  {
    limit: if limit < 0 {
      0
    } else {
      limit
    },
    period: if period < 1 {
      1
    } else {
      period
    },
    ticks: 0,
    accepted: 0,
  }
}

///|
pub fn RateLimiter::allow(self : RateLimiter) -> Bool {
  self.ticks += 1
  let position = (self.ticks - 1) % self.period
  let allowed = position < self.limit
  if allowed {
    self.accepted += 1
  }
  allowed
}

///|
pub fn RateLimiter::accepted(self : RateLimiter) -> Int {
  self.accepted
}

///|
pub fn RateLimiter::reset(self : RateLimiter) -> Unit {
  self.ticks = 0
  self.accepted = 0
}