///|
/// Conjugate Gamma prior for a Poisson segment rate.
pub struct PoissonModel {
  prior_shape : Double
  prior_rate : Double
}

///|
pub fn PoissonModel::new(
  prior_shape : Double,
  prior_rate : Double,
) -> Result[PoissonModel, DetectError] {
  if !finite(prior_shape) || prior_shape <= 0.0 {
    return Err(InvalidPriorStrength)
  }
  if !finite(prior_rate) || prior_rate <= 0.0 {
    return Err(InvalidVariance)
  }
  Ok({ prior_shape, prior_rate, })
}

///|
/// Bounded BOCPD configuration for non-negative integer observations.
pub struct PoissonConfig {
  hazard : Double
  hazard_log : Double
  survival_log : Double
  max_run_length : Int
  model : PoissonModel
}

///|
pub fn PoissonConfig::new(
  hazard : Double,
  max_run_length : Int,
  model : PoissonModel,
) -> Result[PoissonConfig, DetectError] {
  if !finite(hazard) || hazard <= 0.0 || hazard >= 1.0 {
    return Err(InvalidHazard)
  }
  if max_run_length < 2 {
    return Err(InvalidRunLengthLimit)
  }
  Ok({
    hazard,
    hazard_log: @math.ln(hazard),
    survival_log: @math.ln(1.0 - hazard),
    max_run_length,
    model,
  })
}

///|
/// Posterior result for one count observation.
pub struct CountUpdate {
  step : Int
  count : Int
  change_probability : Double
  most_likely_run_length : Int
  posterior_rate : Double
}

///|
pub fn CountUpdate::step(self : CountUpdate) -> Int {
  self.step
}

///|
pub fn CountUpdate::change_probability(self : CountUpdate) -> Double {
  self.change_probability
}

///|
pub fn CountUpdate::most_likely_run_length(self : CountUpdate) -> Int {
  self.most_likely_run_length
}

///|
pub fn CountUpdate::posterior_rate(self : CountUpdate) -> Double {
  self.posterior_rate
}

///|
/// Separate detector avoids making numeric and count-model callers pay for an enum dispatch.
pub struct PoissonDetector {
  config : PoissonConfig
  mut step : Int
  mut log_posterior : Array[Double]
  mut totals : Array[Int]
  mut observations : Array[Int]
}

///|
pub fn PoissonDetector::new(config : PoissonConfig) -> PoissonDetector {
  { config, step: 0, log_posterior: [0.0], totals: [0], observations: [0], }
}

///|
pub fn PoissonDetector::steps(self : PoissonDetector) -> Int {
  self.step
}

///|
pub fn PoissonDetector::run_length_posterior(
  self : PoissonDetector,
) -> Array[Double] {
  self.log_posterior.map(fn(value) { @math.exp(value) })
}

///|
/// Gamma-Poisson posterior predictive probability in log form.
fn PoissonDetector::predictive(
  self : PoissonDetector,
  total : Int,
  observations : Int,
  value : Int,
  value_log_factorial : Double,
) -> Double {
  let model = self.config.model
  let shape = model.prior_shape + total.to_double()
  let rate = model.prior_rate + observations.to_double()
  let next_rate = rate + 1.0
  let numerator = match log_gamma(shape + value.to_double()) {
    Ok(result) => result
    Err(_) => negative_infinity
  }
  let denominator = match log_gamma(shape) {
    Ok(result) => result
    Err(_) => negative_infinity
  }
  numerator -
  denominator -
  value_log_factorial +
  shape * @math.ln(rate / next_rate) +
  value.to_double() * @math.ln(1.0 / next_rate)
}

///|
/// Update with a non-negative count. The change branch always predicts from the prior.
pub fn PoissonDetector::update(
  self : PoissonDetector,
  value : Int,
) -> Result[CountUpdate, DetectError] {
  if value < 0 {
    return Err(InvalidRunLengthLimit)
  }
  let old_size = self.log_posterior.length()
  let new_size = if old_size < self.config.max_run_length {
    old_size + 1
  } else {
    old_size
  }
  let next_log = Array::make(new_size, negative_infinity)
  let next_totals = Array::make(new_size, 0)
  let next_observations = Array::make(new_size, 0)
  let value_log_factorial = match log_factorial(value) {
    Ok(result) => result
    Err(error) => return Err(error)
  }
  let reset_predictive = self.predictive(0, 0, value, value_log_factorial)
  // The posterior is normalized after every update, so this is the full
  // reset branch without repeatedly reducing the same probability mass.
  let changed = self.config.hazard_log + reset_predictive
  for run in 0.. Result[Array[CountUpdate], DetectError] {
  let result : Array[CountUpdate] = []
  for value in values {
    match self.update(value) {
      Ok(update) => result.push(update)
      Err(error) => return Err(error)
    }
  }
  Ok(result)
}