///|
pub(all) struct TrackPoint {
  latitude_value : Double
  longitude_value : Double
  altitude_value : ExactDecimal?
  datetime_value : UtcDateTime?
  reported_speed_knots_value : ExactDecimal?
  quality_value : GgaFixQuality?
  satellites_value : Int?
  usable_value : Bool
  source_record_indexes_value : Array[Int]
} derive(Eq, Debug)

///|
pub(all) struct TrackSegment {
  start_index_value : Int
  end_index_value : Int
  distance_meters_value : Double
  bearing_degrees_value : Double
  duration_millis_value : Int64?
  derived_speed_mps_value : Double?
} derive(Eq, Debug)

///|
pub(all) struct BoundingBox {
  minimum_latitude_value : Double
  maximum_latitude_value : Double
  minimum_longitude_value : Double
  maximum_longitude_value : Double
} derive(Eq, Debug)

///|
pub(all) struct TrackSummary {
  point_count_value : Int
  segment_count_value : Int
  total_distance_meters_value : Double
  total_duration_millis_value : Int64?
  bounds_value : BoundingBox?
} derive(Eq, Debug)

///|
pub(all) struct Track {
  points_value : Array[TrackPoint]
  segments_value : Array[TrackSegment]
  summary_value : TrackSummary
} derive(Eq, Debug)

///|
pub fn TrackPoint::new(
  latitude : Double,
  longitude : Double,
  altitude? : ExactDecimal? = None,
  datetime? : UtcDateTime? = None,
  reported_speed_knots? : ExactDecimal? = None,
  quality? : GgaFixQuality? = None,
  satellites? : Int? = None,
  usable? : Bool = true,
  source_record_indexes? : Array[Int] = [],
) -> Result[TrackPoint, NmeaError] {
  match validate_position(latitude, longitude) {
    Err(error) => Err(error)
    Ok(_) =>
      Ok({
        latitude_value: latitude,
        longitude_value: longitude,
        altitude_value: altitude,
        datetime_value: datetime,
        reported_speed_knots_value: reported_speed_knots,
        quality_value: quality,
        satellites_value: satellites,
        usable_value: usable,
        source_record_indexes_value: source_record_indexes.copy(),
      })
  }
}

///|
pub fn TrackPoint::from_fused(fix : FusedFix) -> Result[TrackPoint, NmeaError] {
  let latitude = match fix.latitude_value {
    Some(value) => value
    None => return Err(geographic_error("fused fix has no latitude"))
  }
  let longitude = match fix.longitude_value {
    Some(value) => value
    None => return Err(geographic_error("fused fix has no longitude"))
  }
  let datetime = match fix.date_value {
    Some(date) => Some(UtcDateTime::new(date, fix.time_value))
    None => None
  }
  TrackPoint::new(
    latitude,
    longitude,
    altitude=fix.altitude_value,
    datetime~,
    reported_speed_knots=fix.speed_knots_value,
    quality=fix.quality_value,
    satellites=fix.satellites_value,
    usable=fix.usable_value,
    source_record_indexes=fix.source_record_indexes_value,
  )
}

///|
fn build_segment(
  start : TrackPoint,
  end : TrackPoint,
  start_index : Int,
) -> Result[TrackSegment, NmeaError] {
  let distance = match
    haversine_distance_meters(
      start.latitude_value,
      start.longitude_value,
      end.latitude_value,
      end.longitude_value,
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let bearing = match
    initial_bearing_degrees(
      start.latitude_value,
      start.longitude_value,
      end.latitude_value,
      end.longitude_value,
    ) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let duration = match (start.datetime_value, end.datetime_value) {
    (Some(first), Some(second)) => Some(second.millis_since(first))
    _ => None
  }
  let derived_speed = match duration {
    Some(value) if value > 0L =>
      match speed_meters_per_second(distance, elapsed_millis=value) {
        Ok(speed) => Some(speed)
        Err(_) => None
      }
    _ => None
  }
  Ok({
    start_index_value: start_index,
    end_index_value: start_index + 1,
    distance_meters_value: distance,
    bearing_degrees_value: bearing,
    duration_millis_value: duration,
    derived_speed_mps_value: derived_speed,
  })
}

///|
fn track_bounds(points : Array[TrackPoint]) -> BoundingBox? {
  if points.length() == 0 {
    return None
  }
  let mut minimum_latitude = points[0].latitude_value
  let mut maximum_latitude = points[0].latitude_value
  let mut minimum_longitude = points[0].longitude_value
  let mut maximum_longitude = points[0].longitude_value
  for index = 1; index < points.length(); index = index + 1 {
    let point = points[index]
    if point.latitude_value < minimum_latitude {
      minimum_latitude = point.latitude_value
    }
    if point.latitude_value > maximum_latitude {
      maximum_latitude = point.latitude_value
    }
    if point.longitude_value < minimum_longitude {
      minimum_longitude = point.longitude_value
    }
    if point.longitude_value > maximum_longitude {
      maximum_longitude = point.longitude_value
    }
  }
  Some({
    minimum_latitude_value: minimum_latitude,
    maximum_latitude_value: maximum_latitude,
    minimum_longitude_value: minimum_longitude,
    maximum_longitude_value: maximum_longitude,
  })
}

///|
pub fn Track::from_points(
  points : Array[TrackPoint],
) -> Result[Track, NmeaError] {
  let stored_points = points.copy()
  let segments : Array[TrackSegment] = []
  for index = 0; index + 1 < stored_points.length(); index = index + 1 {
    match build_segment(stored_points[index], stored_points[index + 1], index) {
      Ok(segment) => segments.push(segment)
      Err(error) => return Err(error)
    }
  }
  let mut total_distance = 0.0
  let mut total_duration : Int64 = 0L
  let mut complete_duration = segments.length() > 0
  for segment in segments {
    total_distance = total_distance + segment.distance_meters_value
    match segment.duration_millis_value {
      Some(duration) => total_duration = total_duration + duration
      None => complete_duration = false
    }
  }
  let summary = {
    point_count_value: stored_points.length(),
    segment_count_value: segments.length(),
    total_distance_meters_value: total_distance,
    total_duration_millis_value: if complete_duration {
      Some(total_duration)
    } else {
      None
    },
    bounds_value: track_bounds(stored_points),
  }
  Ok({
    points_value: stored_points,
    segments_value: segments,
    summary_value: summary,
  })
}

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

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

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

///|
pub fn TrackPoint::datetime(self : TrackPoint) -> UtcDateTime? {
  self.datetime_value
}

///|
pub fn TrackPoint::reported_speed_knots(self : TrackPoint) -> ExactDecimal? {
  self.reported_speed_knots_value
}

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

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

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

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

///|
pub fn TrackSegment::start_index(self : TrackSegment) -> Int {
  self.start_index_value
}

///|
pub fn TrackSegment::end_index(self : TrackSegment) -> Int {
  self.end_index_value
}

///|
pub fn TrackSegment::distance_meters(self : TrackSegment) -> Double {
  self.distance_meters_value
}

///|
pub fn TrackSegment::bearing_degrees(self : TrackSegment) -> Double {
  self.bearing_degrees_value
}

///|
pub fn TrackSegment::duration_millis(self : TrackSegment) -> Int64? {
  self.duration_millis_value
}

///|
pub fn TrackSegment::derived_speed_mps(self : TrackSegment) -> Double? {
  self.derived_speed_mps_value
}

///|
pub fn BoundingBox::minimum_latitude(self : BoundingBox) -> Double {
  self.minimum_latitude_value
}

///|
pub fn BoundingBox::new(
  minimum_latitude : Double,
  maximum_latitude : Double,
  minimum_longitude : Double,
  maximum_longitude : Double,
) -> Result[BoundingBox, NmeaError] {
  match validate_position(minimum_latitude, minimum_longitude) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match validate_position(maximum_latitude, maximum_longitude) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  if minimum_latitude > maximum_latitude ||
    minimum_longitude > maximum_longitude {
    return Err(geographic_error("bounding box minimum exceeds maximum"))
  }
  Ok({
    minimum_latitude_value: minimum_latitude,
    maximum_latitude_value: maximum_latitude,
    minimum_longitude_value: minimum_longitude,
    maximum_longitude_value: maximum_longitude,
  })
}

///|
pub fn BoundingBox::maximum_latitude(self : BoundingBox) -> Double {
  self.maximum_latitude_value
}

///|
pub fn BoundingBox::minimum_longitude(self : BoundingBox) -> Double {
  self.minimum_longitude_value
}

///|
pub fn BoundingBox::maximum_longitude(self : BoundingBox) -> Double {
  self.maximum_longitude_value
}

///|
pub fn TrackSummary::point_count(self : TrackSummary) -> Int {
  self.point_count_value
}

///|
pub fn TrackSummary::segment_count(self : TrackSummary) -> Int {
  self.segment_count_value
}

///|
pub fn TrackSummary::total_distance_meters(self : TrackSummary) -> Double {
  self.total_distance_meters_value
}

///|
pub fn TrackSummary::total_duration_millis(self : TrackSummary) -> Int64? {
  self.total_duration_millis_value
}

///|
pub fn TrackSummary::bounds(self : TrackSummary) -> BoundingBox? {
  self.bounds_value
}

///|
pub fn Track::points(self : Track) -> Array[TrackPoint] {
  self.points_value.copy()
}

///|
pub fn Track::segments(self : Track) -> Array[TrackSegment] {
  self.segments_value.copy()
}

///|
pub fn Track::summary(self : Track) -> TrackSummary {
  self.summary_value
}