///|
/// Common summaries for distributions used by applications without requiring
/// callers to know the concrete conjugate model implementation.
pub enum DistributionKind {
  Bernoulli
  Count
  Continuous
  Categorical
} derive(Debug, Eq, ToJson, FromJson)

///|
pub struct DistributionSummary {
  kind : DistributionKind
  mean : Double
  variance : Double
  support_lower : Double
  support_upper : Double?
} derive(Debug, Eq, ToJson, FromJson)

///|
pub fn DistributionSummary::new(
  kind : DistributionKind,
  mean : Double,
  variance : Double,
  lower : Double,
  upper : Double?,
) -> DistributionSummary {
  if variance < 0.0 {
    abort("DistributionSummary: variance must be non-negative")
  }
  { kind, mean, variance, support_lower: lower, support_upper: upper }
}

///|
pub fn DistributionSummary::kind(
  self : DistributionSummary,
) -> DistributionKind {
  self.kind
}

///|
pub fn DistributionSummary::mean(self : DistributionSummary) -> Double {
  self.mean
}

///|
pub fn DistributionSummary::variance(self : DistributionSummary) -> Double {
  self.variance
}

///|
pub fn DistributionSummary::standard_deviation(
  self : DistributionSummary,
) -> Double {
  self.variance.sqrt()
}

///|
pub fn DistributionSummary::support_lower(self : DistributionSummary) -> Double {
  self.support_lower
}

///|
pub fn DistributionSummary::support_upper(
  self : DistributionSummary,
) -> Double? {
  self.support_upper
}

///|
pub fn BetaBinomial::summary(self : BetaBinomial) -> DistributionSummary {
  DistributionSummary::new(
    Bernoulli,
    self.posterior_mean(),
    self.posterior_variance(),
    0.0,
    Some(1.0),
  )
}

///|
pub fn GammaPoisson::summary(self : GammaPoisson) -> DistributionSummary {
  DistributionSummary::new(
    Count,
    self.posterior_mean(),
    self.posterior_variance(),
    0.0,
    None,
  )
}

///|
pub fn NormalNormal::summary(self : NormalNormal) -> DistributionSummary {
  DistributionSummary::new(
    Continuous,
    self.posterior_mean(),
    self.posterior_variance(),
    -1.0 / 0.0,
    None,
  )
}

///|
pub fn DirichletMultinomial::summary(
  self : DirichletMultinomial,
) -> Array[DistributionSummary] {
  self
  .posterior_mean()
  .mapi(fn(index, value) {
    DistributionSummary::new(
      Categorical,
      value,
      self.posterior_variance()[index],
      0.0,
      Some(1.0),
    )
  })
}

///|
pub fn bernoulli_summary(probability : Double) -> DistributionSummary {
  if probability < 0.0 || probability > 1.0 {
    abort("Bernoulli::to_summary: probability out of range")
  }
  DistributionSummary::new(
    Bernoulli,
    probability,
    probability * (1.0 - probability),
    0.0,
    Some(1.0),
  )
}

///|
pub fn poisson_summary(rate : Double) -> DistributionSummary {
  if rate < 0.0 {
    abort("poisson_summary: rate must be non-negative")
  }
  DistributionSummary::new(Count, rate, rate, 0.0, None)
}

///|
pub fn normal_summary(
  mean_value : Double,
  variance_value : Double,
) -> DistributionSummary {
  if variance_value < 0.0 {
    abort("normal_summary: variance must be non-negative")
  }
  DistributionSummary::new(
    Continuous,
    mean_value,
    variance_value,
    0.0 - 1.0 / 0.0,
    None,
  )
}

///|
pub fn categorical_summary(
  probabilities : Array[Double],
) -> Array[DistributionSummary] {
  let normalized = normalize_probabilities(probabilities)
  normalized.map(fn(probability) {
    DistributionSummary::new(
      Categorical,
      probability,
      probability * (1.0 - probability),
      0.0,
      Some(1.0),
    )
  })
}

///|
pub fn entropy_bernoulli(probability : Double) -> Double {
  if probability <= 0.0 || probability >= 1.0 {
    0.0
  } else {
    probability * (1.0 - probability)
  }
}

///|
pub fn gini_impurity(probabilities : Array[Double]) -> Double {
  let normalized = normalize_probabilities(probabilities)
  1.0 - normalized.fold(init=0.0, (total, value) => total + value * value)
}