///|
/// Severity attached to a machine-readable validation issue.
pub(all) enum ContractSeverity {
  Info
  Warning
  Error
} derive(Debug, Eq)

///|
pub struct ContractIssue {
  code : String
  severity : ContractSeverity
  message : String
} derive(Debug)

///|
pub fn ContractIssue::new(
  code : String,
  severity : ContractSeverity,
  message : String,
) -> ContractIssue {
  { code, severity, message }
}

///|
pub fn ContractIssue::code(self : ContractIssue) -> String {
  self.code
}

///|
pub fn ContractIssue::severity(self : ContractIssue) -> ContractSeverity {
  self.severity
}

///|
pub fn ContractIssue::message(self : ContractIssue) -> String {
  self.message
}

///|
pub struct ValidationReport {
  name : String
  mut checks : Int
  mut passed : Int
  issues : Array[ContractIssue]
}

///|
pub fn ValidationReport::new(name : String) -> ValidationReport {
  { name, checks: 0, passed: 0, issues: [] }
}

///|
pub fn ValidationReport::check(
  self : ValidationReport,
  code : String,
  condition : Bool,
  severity : ContractSeverity,
  message : String,
) -> Bool {
  self.checks = self.checks + 1
  if condition {
    self.passed = self.passed + 1
    true
  } else {
    self.issues.push(ContractIssue::new(code, severity, message))
    false
  }
}

///|
pub fn ValidationReport::name(self : ValidationReport) -> String {
  self.name
}

///|
pub fn ValidationReport::checks(self : ValidationReport) -> Int {
  self.checks
}

///|
pub fn ValidationReport::passed(self : ValidationReport) -> Int {
  self.passed
}

///|
pub fn ValidationReport::failed(self : ValidationReport) -> Int {
  self.checks - self.passed
}

///|
pub fn ValidationReport::issues(
  self : ValidationReport,
) -> Array[ContractIssue] {
  self.issues.copy()
}

///|
pub fn ValidationReport::is_valid(self : ValidationReport) -> Bool {
  self.failed() == 0
}

///|
pub fn ValidationReport::score(self : ValidationReport) -> Double {
  if self.checks == 0 {
    1.0
  } else {
    self.passed.to_double() / self.checks.to_double()
  }
}

///|
pub fn ValidationReport::reset(self : ValidationReport) -> Unit {
  self.checks = 0
  self.passed = 0
  self.issues.clear()
}

///|
pub fn validate_matrix(
  name : String,
  matrix : Matrix,
  expected_rows : Int,
  expected_cols : Int,
) -> ValidationReport {
  let report = ValidationReport::new(name)
  report.check(
    "shape",
    matrix.rows() == expected_rows && matrix.cols() == expected_cols,
    Error,
    "matrix shape mismatch",
  )
  |> ignore
  report.check(
    "finite",
    matrix.is_finite(),
    Error,
    "matrix contains non-finite values",
  )
  |> ignore
  if expected_rows == expected_cols {
    report.check("square", matrix.is_square(), Error, "matrix must be square")
    |> ignore
    report.check(
      "psd",
      covariance_is_psd(matrix, 0.001),
      Warning,
      "matrix is not positive semidefinite",
    )
    |> ignore
  }
  report
}

///|
pub fn validate_state(
  name : String,
  state : Array[Double],
  covariance : Matrix,
) -> ValidationReport {
  let report = ValidationReport::new(name)
  report.check("state_nonempty", state.length() > 0, Error, "state is empty")
  |> ignore
  report.check(
    "state_finite",
    vector_is_finite(state),
    Error,
    "state contains non-finite values",
  )
  |> ignore
  report.check(
    "covariance_shape",
    covariance.rows() == state.length() && covariance.cols() == state.length(),
    Error,
    "covariance shape mismatch",
  )
  |> ignore
  report.check(
    "covariance_finite",
    covariance.is_finite(),
    Error,
    "covariance is non-finite",
  )
  |> ignore
  report.check(
    "covariance_psd",
    covariance_is_psd(covariance, 0.001),
    Warning,
    "covariance is not PSD",
  )
  |> ignore
  report
}

///|
pub fn validate_packet(
  name : String,
  packet : ObservationPacket,
  dimension : Int,
) -> ValidationReport {
  let report = ValidationReport::new(name)
  report.check(
    "valid",
    packet.is_valid(),
    Error,
    "packet failed finite/shape validation",
  )
  |> ignore
  report.check(
    "dimension",
    packet.values().length() == dimension,
    Error,
    "packet dimension mismatch",
  )
  |> ignore
  report.check(
    "timestamp",
    packet.timestamp() >= 0,
    Warning,
    "timestamp is negative",
  )
  |> ignore
  report
}

///|
pub fn validate_sensor_configuration(
  configuration : SensorConfiguration,
) -> ValidationReport {
  let report = ValidationReport::new(configuration.name())
  report.check(
    "dimension",
    configuration.dimension() > 0,
    Error,
    "sensor dimension must be positive",
  )
  |> ignore
  report.check(
    "period",
    configuration.period() > 0,
    Error,
    "sensor period must be positive",
  )
  |> ignore
  report.check(
    "timeout",
    configuration.timeout() >= configuration.period(),
    Warning,
    "sensor timeout is too short",
  )
  |> ignore
  report.check(
    "covariance",
    covariance_is_psd(configuration.covariance(), 0.001),
    Warning,
    "sensor covariance is invalid",
  )
  |> ignore
  report
}

///|
pub fn validate_trajectory(
  name : String,
  points : Array[TrajectoryPoint],
) -> ValidationReport {
  let report = ValidationReport::new(name)
  report.check("nonempty", points.length() > 0, Warning, "trajectory is empty")
  |> ignore
  let mut monotonic = true
  let mut finite = true
  for i, point in points {
    if !point.is_valid() {
      finite = false
    }
    if i > 0 && point.timestamp() < points[i - 1].timestamp() {
      monotonic = false
    }
  }
  report.check("finite", finite, Error, "trajectory contains an invalid point")
  |> ignore
  report.check(
    "timestamps",
    monotonic,
    Error,
    "trajectory timestamps are not monotonic",
  )
  |> ignore
  report
}

///|
pub fn combine_validation_reports(
  name : String,
  reports : Array[ValidationReport],
) -> ValidationReport {
  let result = ValidationReport::new(name)
  for report in reports {
    for issue in report.issues() {
      result.checks = result.checks + 1
      result.issues.push(issue)
    }
    result.checks = result.checks + report.passed()
    result.passed = result.passed + report.passed()
  }
  result
}

///|
pub fn validation_summary(report : ValidationReport) -> String {
  report.name() +
  ": " +
  report.passed().to_string() +
  "/" +
  report.checks().to_string() +
  " checks passed"
}

///|
pub fn validation_has_error(report : ValidationReport) -> Bool {
  for issue in report.issues() {
    if issue.severity() is Error {
      return true
    }
  }
  false
}

///|
pub fn validation_has_warning(report : ValidationReport) -> Bool {
  for issue in report.issues() {
    if issue.severity() is Warning {
      return true
    }
  }
  false
}