///|
pub(all) enum CoordinateAxis {
  Latitude
  Longitude
} derive(Eq, Debug)

///|
pub(all) struct Coordinate {
  axis_value : CoordinateAxis
  degrees_value : Double
  wire_value : ExactDecimal
  hemisphere_value : UInt16
} derive(Eq, Debug)

///|
fn coordinate_error(source : SourceRef, message : String) -> NmeaError {
  NmeaError::from_diagnostic(
    Diagnostic::new(CoordinateInvalid, Error, message, source),
  )
}

///|
pub fn Coordinate::axis(self : Coordinate) -> CoordinateAxis {
  self.axis_value
}

///|
pub fn Coordinate::degrees(self : Coordinate) -> Double {
  self.degrees_value
}

///|
pub fn Coordinate::wire(self : Coordinate) -> ExactDecimal {
  self.wire_value
}

///|
pub fn Coordinate::hemisphere(self : Coordinate) -> UInt16 {
  self.hemisphere_value
}

///|
/// Renders the axis-specific degree width used by NMEA degree-minute fields.
pub fn coordinate_wire_text(value : Coordinate) -> String {
  let text = value.wire_value.to_string()
  let required_whole_digits = match value.axis_value {
    Latitude => 4
    Longitude => 5
  }
  let mut dot = text.length()
  for index = 0; index < text.length(); index = index + 1 {
    if text[index].to_int() == 46 {
      dot = index
      break
    }
  }
  if dot >= required_whole_digits {
    text
  } else {
    String::repeat("0", required_whole_digits - dot) + text
  }
}

///|
fn parse_coordinate(
  value : String,
  hemisphere : String,
  axis : CoordinateAxis,
  source : SourceRef,
) -> Result[Coordinate, NmeaError] {
  if hemisphere.length() != 1 {
    return Err(
      coordinate_error(source, "coordinate hemisphere must be one letter"),
    )
  }
  let hemi = hemisphere[0]
  let valid_hemisphere = match axis {
    Latitude => hemi == 78 || hemi == 83
    Longitude => hemi == 69 || hemi == 87
  }
  if !valid_hemisphere {
    return Err(
      coordinate_error(source, "coordinate hemisphere is invalid for axis"),
    )
  }
  let degree_digits = match axis {
    Latitude => 2
    Longitude => 3
  }
  if value.length() <= degree_digits + 2 {
    return Err(
      coordinate_error(source, "coordinate is missing degree-minute digits"),
    )
  }
  let degree_text = value[:degree_digits].to_owned()
  let minute_text = value[degree_digits:].to_owned()
  let degrees = match
    FieldCursor::new([degree_text]).integer(0, min=0, max=180) {
    Ok(value) => value
    Err(_) =>
      return Err(coordinate_error(source, "coordinate degrees are invalid"))
  }
  let minutes = match ExactDecimal::parse(minute_text, max_scale=9) {
    Ok(value) => value.to_double()
    Err(_) =>
      return Err(coordinate_error(source, "coordinate minutes are invalid"))
  }
  if minutes < 0.0 || minutes >= 60.0 {
    return Err(coordinate_error(source, "coordinate minutes must be below 60"))
  }
  if (axis is Latitude && degrees > 90) || (axis is Longitude && degrees > 180) {
    return Err(
      coordinate_error(source, "coordinate degrees exceed axis bounds"),
    )
  }
  if (
      (axis is Latitude && degrees == 90) ||
      (axis is Longitude && degrees == 180)
    ) &&
    minutes != 0.0 {
    return Err(
      coordinate_error(source, "a pole or antimeridian must have zero minutes"),
    )
  }
  let magnitude = degrees.to_double() + minutes / 60.0
  let signed = if hemi == 83 || hemi == 87 { -magnitude } else { magnitude }
  Ok({
    axis_value: axis,
    degrees_value: signed,
    wire_value: match ExactDecimal::parse(value) {
      Ok(value) => value
      Err(_) =>
        return Err(coordinate_error(source, "coordinate wire value is invalid"))
    },
    hemisphere_value: hemi,
  })
}

///|
pub fn parse_latitude(
  value : String,
  hemisphere : String,
  source : SourceRef,
) -> Result[Coordinate, NmeaError] {
  parse_coordinate(value, hemisphere, Latitude, source)
}

///|
pub fn parse_longitude(
  value : String,
  hemisphere : String,
  source : SourceRef,
) -> Result[Coordinate, NmeaError] {
  parse_coordinate(value, hemisphere, Longitude, source)
}