///|
pub let mean_earth_radius_meters : Double = 6371008.8

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

///|
fn finite(value : Double) -> Bool {
  !value.is_nan() && !value.is_inf()
}

///|
fn validate_position(
  latitude : Double,
  longitude : Double,
) -> Result[Unit, NmeaError] {
  if !finite(latitude) || !finite(longitude) {
    return Err(geographic_error("coordinate components must be finite"))
  }
  if latitude < -90.0 || latitude > 90.0 {
    return Err(geographic_error("latitude must be between -90 and 90 degrees"))
  }
  if longitude < -180.0 || longitude > 180.0 {
    return Err(
      geographic_error("longitude must be between -180 and 180 degrees"),
    )
  }
  Ok(())
}

///|
fn degrees_to_radians(value : Double) -> Double {
  value * @math.PI / 180.0
}

///|
pub fn haversine_distance_meters(
  latitude1 : Double,
  longitude1 : Double,
  latitude2 : Double,
  longitude2 : Double,
) -> Result[Double, NmeaError] {
  match validate_position(latitude1, longitude1) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match validate_position(latitude2, longitude2) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let phi1 = degrees_to_radians(latitude1)
  let phi2 = degrees_to_radians(latitude2)
  let delta_phi = degrees_to_radians(latitude2 - latitude1)
  let delta_lambda = degrees_to_radians(longitude2 - longitude1)
  let sin_phi = @math.sin(delta_phi / 2.0)
  let sin_lambda = @math.sin(delta_lambda / 2.0)
  let raw_a = sin_phi * sin_phi +
    @math.cos(phi1) * @math.cos(phi2) * sin_lambda * sin_lambda
  let a = if raw_a < 0.0 { 0.0 } else if raw_a > 1.0 { 1.0 } else { raw_a }
  let central_angle = 2.0 * @math.atan2(a.sqrt(), (1.0 - a).sqrt())
  Ok(mean_earth_radius_meters * central_angle)
}

///|
pub fn initial_bearing_degrees(
  latitude1 : Double,
  longitude1 : Double,
  latitude2 : Double,
  longitude2 : Double,
) -> Result[Double, NmeaError] {
  match validate_position(latitude1, longitude1) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match validate_position(latitude2, longitude2) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let phi1 = degrees_to_radians(latitude1)
  let phi2 = degrees_to_radians(latitude2)
  let delta_lambda = degrees_to_radians(longitude2 - longitude1)
  let y = @math.sin(delta_lambda) * @math.cos(phi2)
  let x = @math.cos(phi1) * @math.sin(phi2) -
    @math.sin(phi1) * @math.cos(phi2) * @math.cos(delta_lambda)
  let raw = @math.atan2(y, x) * 180.0 / @math.PI
  let normalized = raw % 360.0
  Ok(if normalized < 0.0 { normalized + 360.0 } else { normalized })
}

///|
pub fn speed_meters_per_second(
  distance_meters : Double,
  elapsed_millis~ : Int64,
) -> Result[Double, NmeaError] {
  if !finite(distance_meters) || distance_meters < 0.0 {
    return Err(geographic_error("distance must be finite and non-negative"))
  }
  if elapsed_millis <= 0L {
    return Err(geographic_error("elapsed duration must be positive"))
  }
  Ok(distance_meters * 1000.0 / elapsed_millis.to_double())
}