///|
pub(all) enum FusionSource {
  GgaSource
  RmcSource
  VtgSource
} derive(Eq, Debug)

///|
pub(all) struct FieldProvenance {
  field_value : String
  source_value : FusionSource
  record_index_value : Int
} derive(Eq, Debug)

///|
pub(all) struct FusedFix {
  time_value : UtcTime
  date_value : UtcDate?
  latitude_value : Double?
  longitude_value : Double?
  altitude_value : ExactDecimal?
  quality_value : GgaFixQuality?
  satellites_value : Int?
  speed_knots_value : ExactDecimal?
  course_value : ExactDecimal?
  usable_value : Bool
  provenance_value : Array[FieldProvenance]
  diagnostics_value : Array[Diagnostic]
  source_record_indexes_value : Array[Int]
} derive(Eq, Debug)

///|
struct FusionGroup {
  time_value : UtcTime
  date_value : UtcDate?
  mut gga_value : (GgaRecord, Int)?
  mut rmc_value : (RmcRecord, Int)?
  mut vtg_value : (VtgRecord, Int)?
  diagnostics_value : Array[Diagnostic]
  source_record_indexes_value : Array[Int]
}

///|
pub struct FixFusion {
  groups_value : Array[FusionGroup]
}

///|
fn same_utc_time(left : UtcTime, right : UtcTime) -> Bool {
  left.hour_value == right.hour_value &&
  left.minute_value == right.minute_value &&
  left.second_value == right.second_value &&
  left.nanosecond_value == right.nanosecond_value
}

///|
fn fusion_diagnostic(record_index : Int, message : String) -> Diagnostic {
  Diagnostic::new(
    FusionConflict,
    Error,
    message,
    SourceRef::sentence(record_index),
  )
}

///|
pub fn FixFusion::new() -> FixFusion {
  { groups_value: [] }
}

///|
fn FixFusion::find_group(
  self : FixFusion,
  time : UtcTime,
  date : UtcDate?,
) -> Int? {
  for index = 0; index < self.groups_value.length(); index = index + 1 {
    let group = self.groups_value[index]
    if group.date_value == date && same_utc_time(group.time_value, time) {
      return Some(index)
    }
  }
  None
}

///|
fn FixFusion::group_for(
  self : FixFusion,
  time : UtcTime,
  date : UtcDate?,
) -> FusionGroup {
  match self.find_group(time, date) {
    Some(index) => self.groups_value[index]
    None => {
      let group = {
        time_value: time,
        date_value: date,
        gga_value: None,
        rmc_value: None,
        vtg_value: None,
        diagnostics_value: [],
        source_record_indexes_value: [],
      }
      self.groups_value.push(group)
      group
    }
  }
}

///|
pub fn FixFusion::add_gga(
  self : FixFusion,
  record : GgaRecord,
  record_index~ : Int,
  date? : UtcDate? = None,
) -> Unit {
  let group = self.group_for(record.time_value, date)
  group.source_record_indexes_value.push(record_index)
  match group.gga_value {
    Some(_) =>
      group.diagnostics_value.push(
        fusion_diagnostic(record_index, "duplicate GGA evidence at exact time"),
      )
    None => group.gga_value = Some((record, record_index))
  }
}

///|
pub fn FixFusion::add_rmc(
  self : FixFusion,
  record : RmcRecord,
  record_index~ : Int,
) -> Unit {
  let group = self.group_for(record.time_value, Some(record.date_value))
  group.source_record_indexes_value.push(record_index)
  match group.rmc_value {
    Some(_) =>
      group.diagnostics_value.push(
        fusion_diagnostic(record_index, "duplicate RMC evidence at exact time"),
      )
    None => group.rmc_value = Some((record, record_index))
  }
}

///|
pub fn FixFusion::add_vtg(
  self : FixFusion,
  record : VtgRecord,
  time : UtcTime,
  record_index~ : Int,
  date? : UtcDate? = None,
) -> Unit {
  let group = self.group_for(time, date)
  group.source_record_indexes_value.push(record_index)
  match group.vtg_value {
    Some(_) =>
      group.diagnostics_value.push(
        fusion_diagnostic(record_index, "duplicate VTG evidence at exact time"),
      )
    None => group.vtg_value = Some((record, record_index))
  }
}

///|
fn add_provenance(
  values : Array[FieldProvenance],
  field : String,
  source : FusionSource,
  record_index : Int,
) -> Unit {
  values.push({
    field_value: field,
    source_value: source,
    record_index_value: record_index,
  })
}

///|
fn coordinates_conflict(
  left_latitude : Coordinate,
  left_longitude : Coordinate,
  right_latitude : Coordinate,
  right_longitude : Coordinate,
) -> Bool {
  (left_latitude.degrees_value - right_latitude.degrees_value).abs() >
  0.000000001 ||
  (left_longitude.degrees_value - right_longitude.degrees_value).abs() >
  0.000000001
}

///|
fn FusionGroup::build(self : FusionGroup) -> FusedFix {
  let diagnostics = self.diagnostics_value.copy()
  let provenance : Array[FieldProvenance] = []
  let mut latitude : Double? = None
  let mut longitude : Double? = None
  let mut altitude : ExactDecimal? = None
  let mut quality : GgaFixQuality? = None
  let mut satellites : Int? = None
  let mut speed : ExactDecimal? = None
  let mut course : ExactDecimal? = None
  let mut has_valid_status = false

  match self.gga_value {
    Some((gga, index)) => {
      quality = Some(gga.quality_value)
      satellites = Some(gga.satellites_value)
      altitude = gga.altitude_value
      add_provenance(provenance, "fix_quality", GgaSource, index)
      add_provenance(provenance, "satellites", GgaSource, index)
      if altitude is Some(_) {
        add_provenance(provenance, "altitude", GgaSource, index)
      }
      match (gga.latitude_value, gga.longitude_value) {
        (Some(lat), Some(lon)) => {
          latitude = Some(lat.degrees_value)
          longitude = Some(lon.degrees_value)
          has_valid_status = gga.quality_value != InvalidFix
          add_provenance(provenance, "latitude", GgaSource, index)
          add_provenance(provenance, "longitude", GgaSource, index)
        }
        _ => ()
      }
    }
    None => ()
  }

  match self.rmc_value {
    Some((rmc, index)) => {
      add_provenance(provenance, "date", RmcSource, index)
      match (rmc.latitude_value, rmc.longitude_value) {
        (Some(lat), Some(lon)) => {
          if latitude is Some(_) {
            match self.gga_value {
              Some((gga, _)) =>
                match (gga.latitude_value, gga.longitude_value) {
                  (Some(gga_lat), Some(gga_lon)) =>
                    if coordinates_conflict(gga_lat, gga_lon, lat, lon) {
                      diagnostics.push(
                        fusion_diagnostic(
                          index, "GGA and RMC positions conflict at exact time",
                        ),
                      )
                    }
                  _ => ()
                }
              None => ()
            }
          } else {
            latitude = Some(lat.degrees_value)
            longitude = Some(lon.degrees_value)
            add_provenance(provenance, "latitude", RmcSource, index)
            add_provenance(provenance, "longitude", RmcSource, index)
          }
          has_valid_status = has_valid_status || rmc.status_value is Active
        }
        _ => ()
      }
      speed = rmc.speed_knots_value
      course = rmc.course_value
      if speed is Some(_) {
        add_provenance(provenance, "speed_knots", RmcSource, index)
      }
      if course is Some(_) {
        add_provenance(provenance, "course", RmcSource, index)
      }
    }
    None => ()
  }

  match self.vtg_value {
    Some((vtg, index)) => {
      match (speed, vtg.speed_knots_value) {
        (Some(left), Some(right)) =>
          if left.compare(right) != 0 {
            diagnostics.push(
              fusion_diagnostic(
                index, "RMC and VTG speed values conflict at exact time",
              ),
            )
          }
        (None, Some(value)) => {
          speed = Some(value)
          add_provenance(provenance, "speed_knots", VtgSource, index)
        }
        _ => ()
      }
      match (course, vtg.true_course_value) {
        (Some(left), Some(right)) =>
          if left.compare(right) != 0 {
            diagnostics.push(
              fusion_diagnostic(
                index, "RMC and VTG course values conflict at exact time",
              ),
            )
          }
        (None, Some(value)) => {
          course = Some(value)
          add_provenance(provenance, "course", VtgSource, index)
        }
        _ => ()
      }
    }
    None => ()
  }

  {
    time_value: self.time_value,
    date_value: self.date_value,
    latitude_value: latitude,
    longitude_value: longitude,
    altitude_value: altitude,
    quality_value: quality,
    satellites_value: satellites,
    speed_knots_value: speed,
    course_value: course,
    usable_value: latitude is Some(_) &&
    longitude is Some(_) &&
    has_valid_status,
    provenance_value: provenance,
    diagnostics_value: diagnostics,
    source_record_indexes_value: self.source_record_indexes_value.copy(),
  }
}

///|
pub fn FixFusion::finish(self : FixFusion) -> Array[FusedFix] {
  let fixes : Array[FusedFix] = []
  for group in self.groups_value {
    fixes.push(group.build())
  }
  self.groups_value.clear()
  fixes
}

///|
pub fn FieldProvenance::field(self : FieldProvenance) -> String {
  self.field_value
}

///|
pub fn FieldProvenance::source(self : FieldProvenance) -> FusionSource {
  self.source_value
}

///|
pub fn FieldProvenance::record_index(self : FieldProvenance) -> Int {
  self.record_index_value
}

///|
pub fn FusedFix::time(self : FusedFix) -> UtcTime {
  self.time_value
}

///|
pub fn FusedFix::date(self : FusedFix) -> UtcDate? {
  self.date_value
}

///|
pub fn FusedFix::latitude(self : FusedFix) -> Double? {
  self.latitude_value
}

///|
pub fn FusedFix::longitude(self : FusedFix) -> Double? {
  self.longitude_value
}

///|
pub fn FusedFix::altitude(self : FusedFix) -> ExactDecimal? {
  self.altitude_value
}

///|
pub fn FusedFix::quality(self : FusedFix) -> GgaFixQuality? {
  self.quality_value
}

///|
pub fn FusedFix::satellites(self : FusedFix) -> Int? {
  self.satellites_value
}

///|
pub fn FusedFix::speed_knots(self : FusedFix) -> ExactDecimal? {
  self.speed_knots_value
}

///|
pub fn FusedFix::course(self : FusedFix) -> ExactDecimal? {
  self.course_value
}

///|
pub fn FusedFix::usable(self : FusedFix) -> Bool {
  self.usable_value
}

///|
pub fn FusedFix::provenance(self : FusedFix) -> Array[FieldProvenance] {
  self.provenance_value.copy()
}

///|
pub fn FusedFix::diagnostics(self : FusedFix) -> Array[Diagnostic] {
  self.diagnostics_value.copy()
}

///|
pub fn FusedFix::source_record_indexes(self : FusedFix) -> Array[Int] {
  self.source_record_indexes_value.copy()
}