///|
pub(all) enum TrackFindingKind {
  NoFixFinding
  LowSatellitesFinding
  LowFixQualityFinding
  NonMonotonicTimeFinding
  LongGapFinding
  CoordinateJumpFinding
  ExcessiveSpeedFinding
  SpeedMismatchFinding
} derive(Eq, Debug)

///|
pub(all) struct TrackFinding {
  kind_value : TrackFindingKind
  severity_value : Severity
  point_index_value : Int?
  segment_index_value : Int?
  diagnostic_value : Diagnostic
} derive(Eq, Debug)

///|
pub(all) struct TrackQualityReport {
  findings_value : Array[TrackFinding]
} derive(Eq, Debug)

///|
pub(all) struct QualityThresholds {
  max_gap_millis_value : Int64
  max_segment_distance_meters_value : Double
  max_speed_mps_value : Double
  max_speed_mismatch_mps_value : Double
  minimum_satellites_value : Int
  minimum_fix_quality_rank_value : Int
} derive(Eq, Debug)

///|
pub(all) struct TrackFilter {
  start_time_value : UtcDateTime?
  end_time_value : UtcDateTime?
  bounds_value : BoundingBox?
  minimum_fix_quality_rank_value : Int?
  maximum_severity_value : Severity?
} derive(Eq, Debug)

///|
fn track_quality_error(message : String) -> NmeaError {
  NmeaError::from_diagnostic(
    Diagnostic::new(TrackQualityIssue, Error, message, SourceRef::sentence(0)),
  )
}

///|
pub fn QualityThresholds::new(
  max_gap_millis? : Int64 = 30000L,
  max_segment_distance_meters? : Double = 1000.0,
  max_speed_mps? : Double = 100.0,
  max_speed_mismatch_mps? : Double = 15.0,
  minimum_satellites? : Int = 4,
  minimum_fix_quality_rank? : Int = 1,
) -> Result[QualityThresholds, NmeaError] {
  if max_gap_millis <= 0L ||
    !finite(max_segment_distance_meters) ||
    max_segment_distance_meters <= 0.0 ||
    !finite(max_speed_mps) ||
    max_speed_mps <= 0.0 ||
    !finite(max_speed_mismatch_mps) ||
    max_speed_mismatch_mps < 0.0 ||
    minimum_satellites < 0 ||
    minimum_fix_quality_rank < 0 {
    return Err(
      track_quality_error("quality thresholds are outside valid bounds"),
    )
  }
  Ok({
    max_gap_millis_value: max_gap_millis,
    max_segment_distance_meters_value: max_segment_distance_meters,
    max_speed_mps_value: max_speed_mps,
    max_speed_mismatch_mps_value: max_speed_mismatch_mps,
    minimum_satellites_value: minimum_satellites,
    minimum_fix_quality_rank_value: minimum_fix_quality_rank,
  })
}

///|
pub fn QualityThresholds::standard() -> QualityThresholds {
  QualityThresholds::new().unwrap()
}

///|
fn fix_quality_rank(quality : GgaFixQuality) -> Int {
  match quality {
    InvalidFix => 0
    GpsFix => 1
    DgpsFix => 2
    PpsFix => 3
    RtkFixed => 5
    RtkFloat => 4
    EstimatedFix => 1
    ManualFix => 1
    SimulationFix => 1
    UnknownFix(_) => 0
  }
}

///|
fn point_source_index(point : TrackPoint, fallback : Int) -> Int {
  if point.source_record_indexes_value.length() > 0 {
    point.source_record_indexes_value[0]
  } else {
    fallback
  }
}

///|
fn quality_finding(
  kind : TrackFindingKind,
  severity : Severity,
  message : String,
  source_index : Int,
  point_index : Int?,
  segment_index : Int?,
) -> TrackFinding {
  {
    kind_value: kind,
    severity_value: severity,
    point_index_value: point_index,
    segment_index_value: segment_index,
    diagnostic_value: Diagnostic::new(
      TrackQualityIssue,
      severity,
      message,
      SourceRef::sentence(source_index),
    ),
  }
}

///|
pub fn analyze_track_quality(
  track : Track,
  thresholds : QualityThresholds,
) -> TrackQualityReport {
  let findings : Array[TrackFinding] = []
  for index = 0; index < track.points_value.length(); index = index + 1 {
    let point = track.points_value[index]
    let source = point_source_index(point, index)
    if !point.usable_value {
      findings.push(
        quality_finding(
          NoFixFinding,
          Error,
          "point is marked as unusable navigation evidence",
          source,
          Some(index),
          None,
        ),
      )
    }
    match point.satellites_value {
      Some(value) if value < thresholds.minimum_satellites_value =>
        findings.push(
          quality_finding(
            LowSatellitesFinding,
            Warning,
            "point satellite count is below the configured minimum",
            source,
            Some(index),
            None,
          ),
        )
      _ => ()
    }
    match point.quality_value {
      Some(value) if fix_quality_rank(value) <
        thresholds.minimum_fix_quality_rank_value =>
        findings.push(
          quality_finding(
            LowFixQualityFinding,
            Warning,
            "point fix quality is below the configured minimum",
            source,
            Some(index),
            None,
          ),
        )
      _ => ()
    }
  }
  for index = 0; index < track.segments_value.length(); index = index + 1 {
    let segment = track.segments_value[index]
    let end_point = track.points_value[segment.end_index_value]
    let source = point_source_index(end_point, segment.end_index_value)
    match segment.duration_millis_value {
      Some(value) if value <= 0L =>
        findings.push(
          quality_finding(
            NonMonotonicTimeFinding,
            Error,
            "segment time does not increase",
            source,
            None,
            Some(index),
          ),
        )
      Some(value) if value > thresholds.max_gap_millis_value =>
        findings.push(
          quality_finding(
            LongGapFinding,
            Warning,
            "segment duration exceeds the configured gap",
            source,
            None,
            Some(index),
          ),
        )
      _ => ()
    }
    if segment.distance_meters_value >
      thresholds.max_segment_distance_meters_value {
      findings.push(
        quality_finding(
          CoordinateJumpFinding,
          Warning,
          "segment distance exceeds the configured jump",
          source,
          None,
          Some(index),
        ),
      )
    }
    match segment.derived_speed_mps_value {
      Some(value) if value > thresholds.max_speed_mps_value =>
        findings.push(
          quality_finding(
            ExcessiveSpeedFinding,
            Error,
            "segment derived speed exceeds the configured maximum",
            source,
            None,
            Some(index),
          ),
        )
      _ => ()
    }
    match
      (segment.derived_speed_mps_value, end_point.reported_speed_knots_value) {
      (Some(derived), Some(reported)) => {
        let reported_mps = reported.to_double() * 0.5144444444444445
        if (derived - reported_mps).abs() >
          thresholds.max_speed_mismatch_mps_value {
          findings.push(
            quality_finding(
              SpeedMismatchFinding,
              Warning,
              "reported and derived speed differ beyond the configured tolerance",
              source,
              None,
              Some(index),
            ),
          )
        }
      }
      _ => ()
    }
  }
  { findings_value: findings }
}

///|
pub fn TrackFilter::new(
  start_time? : UtcDateTime? = None,
  end_time? : UtcDateTime? = None,
  bounds? : BoundingBox? = None,
  minimum_fix_quality_rank? : Int? = None,
  maximum_severity? : Severity? = None,
) -> Result[TrackFilter, NmeaError] {
  match (start_time, end_time) {
    (Some(start), Some(end)) if start.compare(end) > 0 =>
      return Err(track_quality_error("filter start time exceeds end time"))
    _ => ()
  }
  match minimum_fix_quality_rank {
    Some(value) if value < 0 =>
      return Err(track_quality_error("minimum fix quality rank is negative"))
    _ => ()
  }
  Ok({
    start_time_value: start_time,
    end_time_value: end_time,
    bounds_value: bounds,
    minimum_fix_quality_rank_value: minimum_fix_quality_rank,
    maximum_severity_value: maximum_severity,
  })
}

///|
fn severity_rank(severity : Severity) -> Int {
  match severity {
    Info => 0
    Warning => 1
    Error => 2
  }
}

///|
fn point_matches_filter(point : TrackPoint, filter : TrackFilter) -> Bool {
  match filter.start_time_value {
    Some(start) =>
      match point.datetime_value {
        Some(value) if value.compare(start) >= 0 => ()
        _ => return false
      }
    None => ()
  }
  match filter.end_time_value {
    Some(end) =>
      match point.datetime_value {
        Some(value) if value.compare(end) <= 0 => ()
        _ => return false
      }
    None => ()
  }
  match filter.bounds_value {
    Some(bounds) =>
      if point.latitude_value < bounds.minimum_latitude_value ||
        point.latitude_value > bounds.maximum_latitude_value ||
        point.longitude_value < bounds.minimum_longitude_value ||
        point.longitude_value > bounds.maximum_longitude_value {
        return false
      }
    None => ()
  }
  match filter.minimum_fix_quality_rank_value {
    Some(minimum) =>
      match point.quality_value {
        Some(quality) if fix_quality_rank(quality) >= minimum => ()
        _ => return false
      }
    None => ()
  }
  true
}

///|
pub fn filter_track(
  track : Track,
  filter : TrackFilter,
  report? : TrackQualityReport? = None,
) -> Result[Track, NmeaError] {
  let excluded = Array::make(track.points_value.length(), false)
  match (filter.maximum_severity_value, report) {
    (Some(maximum), Some(quality_report)) =>
      for finding in quality_report.findings_value {
        if severity_rank(finding.severity_value) > severity_rank(maximum) {
          match finding.point_index_value {
            Some(index) if index >= 0 && index < excluded.length() =>
              excluded[index] = true
            _ => ()
          }
          match finding.segment_index_value {
            Some(index) if index + 1 >= 0 && index + 1 < excluded.length() =>
              excluded[index + 1] = true
            _ => ()
          }
        }
      }
    _ => ()
  }
  let points : Array[TrackPoint] = []
  for index = 0; index < track.points_value.length(); index = index + 1 {
    let point = track.points_value[index]
    if !excluded[index] && point_matches_filter(point, filter) {
      points.push(point)
    }
  }
  Track::from_points(points)
}

///|
pub fn TrackFinding::kind(self : TrackFinding) -> TrackFindingKind {
  self.kind_value
}

///|
pub fn TrackFinding::severity(self : TrackFinding) -> Severity {
  self.severity_value
}

///|
pub fn TrackFinding::point_index(self : TrackFinding) -> Int? {
  self.point_index_value
}

///|
pub fn TrackFinding::segment_index(self : TrackFinding) -> Int? {
  self.segment_index_value
}

///|
pub fn TrackFinding::diagnostic(self : TrackFinding) -> Diagnostic {
  self.diagnostic_value
}

///|
pub fn TrackQualityReport::findings(
  self : TrackQualityReport,
) -> Array[TrackFinding] {
  self.findings_value.copy()
}