///|
fn integrity_diagnostic(
  message : String,
  source_index? : Int = 0,
) -> Diagnostic {
  Diagnostic::new(
    IntegrityInvalid,
    Error,
    message,
    SourceRef::sentence(source_index),
  )
}

///|
fn string_entry_total(entries : Array[StringCountEntry]) -> Int {
  let mut total = 0
  for entry in entries {
    total += entry.count_value
  }
  total
}

///|
fn checksum_entry_total(entries : Array[ChecksumCountEntry]) -> Int {
  let mut total = 0
  for entry in entries {
    total += entry.count_value
  }
  total
}

///|
pub fn validate_batch_integrity(batch : NmeaBatch) -> Array[Diagnostic] {
  let findings : Array[Diagnostic] = []
  let summary = batch.summary_value
  if summary.total_value != batch.items_value.length() {
    findings.push(integrity_diagnostic("batch total does not match item count"))
  }
  let mut successes = 0
  let mut failures = 0
  for index = 0; index < batch.items_value.length(); index = index + 1 {
    let item = batch.items_value[index]
    if item.record_index_value != index {
      findings.push(
        integrity_diagnostic(
          "batch item index is not contiguous",
          source_index=index,
        ),
      )
    }
    match (item.value_value, item.error_value) {
      (Some(_), None) => successes += 1
      (None, Some(_)) => failures += 1
      _ =>
        findings.push(
          integrity_diagnostic(
            "batch item must contain exactly one value or error",
            source_index=index,
          ),
        )
    }
  }
  if summary.successes_value != successes || summary.failures_value != failures {
    findings.push(
      integrity_diagnostic("batch outcome totals do not match items"),
    )
  }
  if summary.successes_value + summary.failures_value != summary.total_value {
    findings.push(
      integrity_diagnostic("batch success and failure totals do not reconcile"),
    )
  }
  if string_entry_total(summary.formatter_entries_value) != successes ||
    string_entry_total(summary.talker_entries_value) != successes {
    findings.push(
      integrity_diagnostic("batch successful record counts do not reconcile"),
    )
  }
  if string_entry_total(summary.error_entries_value) != failures {
    findings.push(integrity_diagnostic("batch error counts do not reconcile"))
  }
  if checksum_entry_total(summary.checksum_entries_value) != summary.total_value {
    findings.push(
      integrity_diagnostic("batch checksum counts do not reconcile"),
    )
  }
  findings
}

///|
pub fn validate_gsv_integrity(assembly : GsvAssembly) -> Array[Diagnostic] {
  let findings : Array[Diagnostic] = []
  let source = if assembly.source_record_indexes_value.length() > 0 {
    assembly.source_record_indexes_value[0]
  } else {
    0
  }
  if assembly.total_messages_value <= 0 ||
    assembly.source_record_indexes_value.length() !=
    assembly.total_messages_value {
    findings.push(
      integrity_diagnostic(
        "GSV source fragments do not match announced message total",
        source_index=source,
      ),
    )
  }
  if assembly.observations_value.length() != assembly.total_satellites_value {
    findings.push(
      integrity_diagnostic(
        "GSV observations do not match announced satellite total",
        source_index=source,
      ),
    )
  }
  findings
}

///|
fn source_contains(sources : Array[Int], record_index : Int) -> Bool {
  for source in sources {
    if source == record_index {
      return true
    }
  }
  false
}

///|
pub fn validate_fused_fix_integrity(fix : FusedFix) -> Array[Diagnostic] {
  let findings : Array[Diagnostic] = []
  let source = if fix.source_record_indexes_value.length() > 0 {
    fix.source_record_indexes_value[0]
  } else {
    0
  }
  if (fix.latitude_value is Some(_)) != (fix.longitude_value is Some(_)) {
    findings.push(
      integrity_diagnostic(
        "fused position has only one coordinate",
        source_index=source,
      ),
    )
  }
  if fix.usable_value &&
    (fix.latitude_value is None || fix.longitude_value is None) {
    findings.push(
      integrity_diagnostic(
        "usable fused fix has no complete position",
        source_index=source,
      ),
    )
  }
  if fix.source_record_indexes_value.length() > 0 &&
    fix.provenance_value.length() == 0 {
    findings.push(
      integrity_diagnostic(
        "fused fix has sources but no field provenance",
        source_index=source,
      ),
    )
  }
  for provenance in fix.provenance_value {
    if !source_contains(
        fix.source_record_indexes_value,
        provenance.record_index_value,
      ) {
      findings.push(
        integrity_diagnostic(
          "field provenance references an absent source record",
          source_index=provenance.record_index_value,
        ),
      )
    }
  }
  findings
}

///|
pub fn validate_track_integrity(track : Track) -> Array[Diagnostic] {
  let findings : Array[Diagnostic] = []
  let expected_segments = if track.points_value.length() == 0 {
    0
  } else {
    track.points_value.length() - 1
  }
  if track.summary_value.point_count_value != track.points_value.length() {
    findings.push(
      integrity_diagnostic("track summary point count is inconsistent"),
    )
  }
  if track.segments_value.length() != expected_segments ||
    track.summary_value.segment_count_value != track.segments_value.length() {
    findings.push(integrity_diagnostic("track segment counts are inconsistent"))
  }
  let mut distance = 0.0
  for index = 0; index < track.segments_value.length(); index = index + 1 {
    let segment = track.segments_value[index]
    distance = distance + segment.distance_meters_value
    if segment.start_index_value != index ||
      segment.end_index_value != index + 1 {
      findings.push(
        integrity_diagnostic("track segment indexes are inconsistent"),
      )
    }
  }
  if (distance - track.summary_value.total_distance_meters_value).abs() >
    0.000001 {
    findings.push(integrity_diagnostic("track total distance is inconsistent"))
  }
  if (track.points_value.length() == 0) !=
    (track.summary_value.bounds_value is None) {
    findings.push(integrity_diagnostic("track bounds presence is inconsistent"))
  }
  findings
}