///|
pub(all) enum GgaFixQuality {
  InvalidFix
  GpsFix
  DgpsFix
  PpsFix
  RtkFixed
  RtkFloat
  EstimatedFix
  ManualFix
  SimulationFix
  UnknownFix(Int)
} derive(Eq, Debug)

///|
pub(all) struct GgaRecord {
  talker_value : String
  time_value : UtcTime
  latitude_value : Coordinate?
  longitude_value : Coordinate?
  quality_value : GgaFixQuality
  satellites_value : Int
  hdop_value : ExactDecimal?
  altitude_value : ExactDecimal?
  geoid_separation_value : ExactDecimal?
  dgps_age_value : ExactDecimal?
  dgps_station_value : String?
} derive(Eq, Debug)

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

///|
fn gga_quality(value : Int) -> GgaFixQuality {
  match value {
    0 => InvalidFix
    1 => GpsFix
    2 => DgpsFix
    3 => PpsFix
    4 => RtkFixed
    5 => RtkFloat
    6 => EstimatedFix
    7 => ManualFix
    8 => SimulationFix
    other => UnknownFix(other)
  }
}

///|
fn optional_gga_decimal(
  cursor : FieldCursor,
  index : Int,
) -> Result[ExactDecimal?, NmeaError] {
  match cursor.optional(index) {
    Ok(None) => Ok(None)
    Ok(Some(_)) =>
      match cursor.decimal(index) {
        Ok(value) => Ok(Some(value))
        Err(error) => Err(error)
      }
    Err(error) => Err(error)
  }
}

///|
pub fn parse_gga(frame : NmeaFrame) -> Result[GgaRecord, NmeaError] {
  if frame.formatter() != "GGA" {
    return Err(gga_error("frame formatter is not GGA"))
  }
  let cursor = FieldCursor::new(frame.fields())
  let time = match cursor.required(0) {
    Ok(value) => UtcTime::parse(value, SourceRef::field(0, 0))
    Err(error) => Err(error)
  }
  let time = match time {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let quality_value = match cursor.integer(5, min=0, max=255) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let satellites = match cursor.integer(6, min=0, max=255) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let quality = gga_quality(quality_value)
  let latitude_text = match cursor.optional(1) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let latitude_hemisphere = match cursor.optional(2) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let longitude_text = match cursor.optional(3) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let longitude_hemisphere = match cursor.optional(4) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let (latitude, longitude) = if quality_value == 0 {
    (None, None)
  } else {
    let latitude_text = match latitude_text {
      Some(value) => value
      None => return Err(gga_error("valid GGA fix is missing latitude"))
    }
    let latitude_hemisphere = match latitude_hemisphere {
      Some(value) => value
      None =>
        return Err(gga_error("valid GGA fix is missing latitude hemisphere"))
    }
    let longitude_text = match longitude_text {
      Some(value) => value
      None => return Err(gga_error("valid GGA fix is missing longitude"))
    }
    let longitude_hemisphere = match longitude_hemisphere {
      Some(value) => value
      None =>
        return Err(gga_error("valid GGA fix is missing longitude hemisphere"))
    }
    let latitude = match
      parse_latitude(latitude_text, latitude_hemisphere, SourceRef::field(0, 1)) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    let longitude = match
      parse_longitude(
        longitude_text,
        longitude_hemisphere,
        SourceRef::field(0, 3),
      ) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    (Some(latitude), Some(longitude))
  }
  let hdop = match optional_gga_decimal(cursor, 7) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let altitude = match optional_gga_decimal(cursor, 8) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let geoid_separation = match optional_gga_decimal(cursor, 10) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let dgps_age = match optional_gga_decimal(cursor, 12) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let dgps_station = match cursor.optional(13) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  Ok({
    talker_value: frame.talker(),
    time_value: time,
    latitude_value: latitude,
    longitude_value: longitude,
    quality_value: quality,
    satellites_value: satellites,
    hdop_value: hdop,
    altitude_value: altitude,
    geoid_separation_value: geoid_separation,
    dgps_age_value: dgps_age,
    dgps_station_value: dgps_station,
  })
}

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

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

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

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

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

///|
pub fn GgaRecord::hdop(self : GgaRecord) -> ExactDecimal? {
  self.hdop_value
}

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

///|
pub fn GgaRecord::geoid_separation(self : GgaRecord) -> ExactDecimal? {
  self.geoid_separation_value
}

///|
fn gga_decimal_text(value : ExactDecimal?) -> String {
  match value {
    Some(value) => value.to_string()
    None => ""
  }
}

///|
pub fn GgaRecord::encode(self : GgaRecord) -> Result[String, NmeaError] {
  let latitude = match self.latitude_value {
    Some(value) => value.wire().to_string()
    None => ""
  }
  let latitude_hemisphere = match self.latitude_value {
    Some(value) => if value.hemisphere() == 78 { "N" } else { "S" }
    None => ""
  }
  let longitude = match self.longitude_value {
    Some(value) => coordinate_wire_text(value)
    None => ""
  }
  let longitude_hemisphere = match self.longitude_value {
    Some(value) => if value.hemisphere() == 69 { "E" } else { "W" }
    None => ""
  }
  let quality = match self.quality_value {
    InvalidFix => 0
    GpsFix => 1
    DgpsFix => 2
    PpsFix => 3
    RtkFixed => 4
    RtkFloat => 5
    EstimatedFix => 6
    ManualFix => 7
    SimulationFix => 8
    UnknownFix(value) => value
  }
  let frame = NmeaFrame::new(36, self.talker_value, "GGA", [
    pad2(self.time_value.hour()) +
    pad2(self.time_value.minute()) +
    self.time_value.second_wire_value.to_string(),
    latitude,
    latitude_hemisphere,
    longitude,
    longitude_hemisphere,
    quality.to_string(),
    self.satellites_value.to_string(),
    gga_decimal_text(self.hdop_value),
    gga_decimal_text(self.altitude_value),
    "M",
    gga_decimal_text(self.geoid_separation_value),
    "M",
    gga_decimal_text(self.dgps_age_value),
    match self.dgps_station_value {
      Some(value) => value
      None => ""
    },
  ])
  Ok(frame.encode())
}