///|
/// Namespace object for covariance and observation diagnostics.
pub enum KalmanDiagnostics {
  Token
} derive(Debug, Eq)

///|
pub enum CovarianceHealth {
  Healthy
  NonFinite
  NotSquare
  NotSymmetric
  NotPositiveSemidefinite
  IllConditioned
} derive(Debug, Eq)

///|
pub struct CovarianceReport {
  health : CovarianceHealth
  dimension : Int
  symmetry_error : Double
  minimum_eigenvalue : Double
  maximum_eigenvalue : Double
  condition_estimate : Double
  finite : Bool
  positive_diagonal : Bool
} derive(Debug)

///|
pub fn CovarianceReport::health(self : CovarianceReport) -> CovarianceHealth {
  self.health
}

///|
pub fn CovarianceReport::dimension(self : CovarianceReport) -> Int {
  self.dimension
}

///|
pub fn CovarianceReport::symmetry_error(self : CovarianceReport) -> Double {
  self.symmetry_error
}

///|
pub fn CovarianceReport::minimum_eigenvalue(self : CovarianceReport) -> Double {
  self.minimum_eigenvalue
}

///|
pub fn CovarianceReport::maximum_eigenvalue(self : CovarianceReport) -> Double {
  self.maximum_eigenvalue
}

///|
pub fn CovarianceReport::condition_estimate(self : CovarianceReport) -> Double {
  self.condition_estimate
}

///|
pub fn CovarianceReport::is_healthy(self : CovarianceReport) -> Bool {
  self.health is Healthy
}

///|
pub fn CovarianceReport::finite(self : CovarianceReport) -> Bool {
  self.finite
}

///|
pub fn CovarianceReport::positive_diagonal(self : CovarianceReport) -> Bool {
  self.positive_diagonal
}

///|
pub fn KalmanDiagnostics::new() -> KalmanDiagnostics {
  Token
}

///|
/// Inspect symmetry, finite values, diagonal signs, approximate eigenvalues,
/// and conditioning of a covariance matrix.
pub fn KalmanDiagnostics::report(
  self : KalmanDiagnostics,
  covariance : Matrix,
) -> CovarianceReport {
  let _ = self
  let finite = covariance.is_finite()
  let square = covariance.is_square()
  let mut symmetry_error = 0.0
  if square {
    for i in 0.. symmetry_error {
          symmetry_error = difference
        }
      }
    }
  }
  let mut positive_diagonal = square
  if square {
    for i in 0.. 0 {
    minimum = eigenvalues[0]
    maximum = eigenvalues[0]
    for value in eigenvalues {
      if value < minimum {
        minimum = value
      }
      if value > maximum {
        maximum = value
      }
    }
  }
  let condition = if minimum > 0.000000000001 { maximum / minimum } else { 0.0 }
  let health = if !finite {
    NonFinite
  } else if !square {
    NotSquare
  } else if symmetry_error > 0.000001 {
    NotSymmetric
  } else if !positive_diagonal || minimum < -0.000001 {
    NotPositiveSemidefinite
  } else if condition > 100000000.0 {
    IllConditioned
  } else {
    Healthy
  }
  {
    health,
    dimension: covariance.rows(),
    symmetry_error,
    minimum_eigenvalue: minimum,
    maximum_eigenvalue: maximum,
    condition_estimate: condition,
    finite,
    positive_diagonal,
  }
}

///|
/// Compatibility helper for the original API.
pub fn KalmanDiagnostics::check_covariance(
  self : KalmanDiagnostics,
  covariance : Array[Array[Double]],
) -> Bool {
  self.report(Matrix::from_rows(covariance)).is_healthy()
}

///|
/// Return whether a matrix is symmetric positive semidefinite within a
/// caller-supplied tolerance.
pub fn covariance_is_psd(covariance : Matrix, tolerance : Double) -> Bool {
  let report = KalmanDiagnostics::new().report(covariance)
  report.symmetry_error() <= tolerance &&
  report.minimum_eigenvalue() >= -tolerance
}

///|
pub fn covariance_is_positive_definite(
  covariance : Matrix,
  tolerance : Double,
) -> Bool {
  let report = KalmanDiagnostics::new().report(covariance)
  report.symmetry_error() <= tolerance &&
  report.minimum_eigenvalue() > tolerance
}

///|
pub fn covariance_project_psd(covariance : Matrix, floor : Double) -> Matrix {
  let safe_floor = if floor < 0.0 { 0.0 } else { floor }
  let result = covariance.symmetric_part()
  for i in 0.. ignore
    }
  }
  result
}

///|
pub struct InnovationDiagnostics {
  innovation : Array[Double]
  covariance : Matrix
  nis : Double
  whitened_norm : Double
  accepted : Bool
} derive(Debug)

///|
pub fn InnovationDiagnostics::innovation(
  self : InnovationDiagnostics,
) -> Array[Double] {
  self.innovation.copy()
}

///|
pub fn InnovationDiagnostics::covariance(
  self : InnovationDiagnostics,
) -> Matrix {
  self.covariance.copy()
}

///|
pub fn InnovationDiagnostics::nis(self : InnovationDiagnostics) -> Double {
  self.nis
}

///|
pub fn InnovationDiagnostics::whitened_norm(
  self : InnovationDiagnostics,
) -> Double {
  self.whitened_norm
}

///|
pub fn InnovationDiagnostics::accepted(self : InnovationDiagnostics) -> Bool {
  self.accepted
}

///|
pub fn make_innovation_diagnostics(
  innovation : Array[Double],
  covariance : Matrix,
  gate_threshold : Double,
) -> InnovationDiagnostics {
  let nis = match covariance.inverse() {
    None => 0.0
    Some(inverse) => vector_dot(innovation, inverse.multiply_vector(innovation))
  }
  let safe_threshold = if gate_threshold < 0.0 { 0.0 } else { gate_threshold }
  {
    innovation: innovation.copy(),
    covariance: covariance.copy(),
    nis,
    whitened_norm: nis.sqrt(),
    accepted: nis <= safe_threshold,
  }
}

///|
pub(all) enum MissingObservationAction {
  PredictOnly
  InflateAndPredict
  HoldLastEstimate
} derive(Debug, Eq)

///|
pub struct MissingObservationPolicy {
  max_consecutive : Int
  inflate_factor : Double
  action : MissingObservationAction
} derive(Debug)

///|
pub fn MissingObservationPolicy::new(
  max_consecutive : Int,
  inflate_factor : Double,
  action : MissingObservationAction,
) -> MissingObservationPolicy {
  {
    max_consecutive: if max_consecutive < 0 {
      0
    } else {
      max_consecutive
    },
    inflate_factor: if inflate_factor < 1.0 {
      1.0
    } else {
      inflate_factor
    },
    action,
  }
}

///|
pub fn MissingObservationPolicy::max_consecutive(
  self : MissingObservationPolicy,
) -> Int {
  self.max_consecutive
}

///|
pub fn MissingObservationPolicy::inflate_factor(
  self : MissingObservationPolicy,
) -> Double {
  self.inflate_factor
}

///|
pub fn MissingObservationPolicy::action(
  self : MissingObservationPolicy,
) -> MissingObservationAction {
  self.action
}

///|
pub fn handle_missing_observation() -> Unit {
  // Compatibility entry point: a missing sample means predict-only. The
  // policy type above lets applications make the decision explicit.
}

///|
pub fn missing_action(
  policy : MissingObservationPolicy,
  consecutive : Int,
) -> MissingObservationAction {
  if consecutive > policy.max_consecutive {
    HoldLastEstimate
  } else {
    match policy.action {
      PredictOnly => PredictOnly
      InflateAndPredict => InflateAndPredict
      HoldLastEstimate => HoldLastEstimate
    }
  }
}

///|
pub fn covariance_trace(covariance : Matrix) -> Double {
  covariance.trace()
}

///|
pub fn covariance_average_variance(covariance : Matrix) -> Double {
  if !covariance.is_square() || covariance.rows() == 0 {
    return 0.0
  }
  covariance.trace() / covariance.rows().to_double()
}

///|
pub fn covariance_relative_change(
  previous : Matrix,
  current : Matrix,
) -> Double {
  let denominator = previous.frobenius_norm()
  if denominator <= 0.000000000001 {
    current.frobenius_norm()
  } else {
    previous.sub(current).frobenius_norm() / denominator
  }
}

///|
pub fn state_has_valid_uncertainty(
  state : Array[Double],
  covariance : Matrix,
) -> Bool {
  vector_is_finite(state) &&
  covariance.rows() == state.length() &&
  covariance.cols() == state.length() &&
  covariance_is_psd(covariance, 0.000001)
}