///|
pub struct GroundTrackSummary {
  points : Int
  duration_s : Double
  minimum_altitude_km : Double
  maximum_altitude_km : Double
  mean_altitude_km : Double
  maximum_latitude_rad : Double
  longitude_span_rad : Double
  path_length_km : Double
  gap_count : Int
} derive(Debug, Eq)

///|
pub struct GroundTrackSegment {
  points : Array[GroundTrackPoint]
  start_time_s : Double
  end_time_s : Double
  path_length_km : Double
} derive(Debug, Eq)

///|
pub struct GroundTrackBounds {
  minimum_latitude_rad : Double
  maximum_latitude_rad : Double
  minimum_longitude_rad : Double
  maximum_longitude_rad : Double
  minimum_altitude_km : Double
  maximum_altitude_km : Double
} derive(Debug, Eq)

///|
pub struct StateSample {
  time_s : Double
  state : StateVector
} derive(Debug, Eq)

///|
pub struct StateComparison {
  count : Int
  position_rms_km : Double
  velocity_rms_km_s : Double
  position_max_km : Double
  velocity_max_km_s : Double
  time_span_s : Double
} derive(Debug, Eq)

///|
fn track_geodetic(point : GroundTrackPoint) -> Geodetic {
  Geodetic::new(point.latitude_rad, point.longitude_rad, point.altitude_km)
}

///|
fn track_pair_distance(a : GroundTrackPoint, b : GroundTrackPoint) -> Double {
  geodesic_surface_distance_km(track_geodetic(a), track_geodetic(b))
}

///|
pub fn ground_track_path_length(track : Array[GroundTrackPoint]) -> Double {
  if track.length() < 2 {
    0.0
  } else {
    let mut total = 0.0
    for i in 0..<(track.length() - 1) {
      total += track_pair_distance(track[i], track[i + 1])
    }
    total
  }
}

///|
pub fn ground_track_gap_count(
  track : Array[GroundTrackPoint],
  maximum_gap_s : Double,
) -> Int {
  if track.length() < 2 {
    0
  } else {
    let mut count = 0
    for i in 0..<(track.length() - 1) {
      if track[i + 1].time_s - track[i].time_s > maximum_gap_s.max(0.0) {
        count += 1
      }
    }
    count
  }
}

///|
pub fn ground_track_summary(
  track : Array[GroundTrackPoint],
) -> GroundTrackSummary {
  if track.length() == 0 {
    {
      points: 0,
      duration_s: 0.0,
      minimum_altitude_km: 0.0,
      maximum_altitude_km: 0.0,
      mean_altitude_km: 0.0,
      maximum_latitude_rad: 0.0,
      longitude_span_rad: 0.0,
      path_length_km: 0.0,
      gap_count: 0,
    }
  } else {
    let altitudes = track.map(point => point.altitude_km)
    let first = track[0]
    let last = track[track.length() - 1]
    let stats = summarize_samples(altitudes)
    {
      points: track.length(),
      duration_s: last.time_s - first.time_s,
      minimum_altitude_km: stats.minimum,
      maximum_altitude_km: stats.maximum,
      mean_altitude_km: stats.mean,
      maximum_latitude_rad: maximum_latitude(track),
      longitude_span_rad: longitude_span(track),
      path_length_km: ground_track_path_length(track),
      gap_count: ground_track_gap_count(track, 120.0),
    }
  }
}

///|
pub fn ground_track_bounds(
  track : Array[GroundTrackPoint],
) -> GroundTrackBounds {
  if track.length() == 0 {
    {
      minimum_latitude_rad: 0.0,
      maximum_latitude_rad: 0.0,
      minimum_longitude_rad: 0.0,
      maximum_longitude_rad: 0.0,
      minimum_altitude_km: 0.0,
      maximum_altitude_km: 0.0,
    }
  } else {
    let first = track[0]
    let mut min_lat = first.latitude_rad
    let mut max_lat = first.latitude_rad
    let mut min_lon = first.longitude_rad
    let mut max_lon = first.longitude_rad
    let mut min_alt = first.altitude_km
    let mut max_alt = first.altitude_km
    for point in track {
      min_lat = min_lat.min(point.latitude_rad)
      max_lat = max_lat.max(point.latitude_rad)
      min_lon = min_lon.min(point.longitude_rad)
      max_lon = max_lon.max(point.longitude_rad)
      min_alt = min_alt.min(point.altitude_km)
      max_alt = max_alt.max(point.altitude_km)
    }
    {
      minimum_latitude_rad: min_lat,
      maximum_latitude_rad: max_lat,
      minimum_longitude_rad: min_lon,
      maximum_longitude_rad: max_lon,
      minimum_altitude_km: min_alt,
      maximum_altitude_km: max_alt,
    }
  }
}

///|
pub fn track_is_contiguous(
  track : Array[GroundTrackPoint],
  maximum_gap_s : Double,
) -> Bool {
  ground_track_gap_count(track, maximum_gap_s) == 0
}

///|
pub fn track_filter_altitude(
  track : Array[GroundTrackPoint],
  minimum_km : Double,
  maximum_km : Double,
) -> Array[GroundTrackPoint] {
  track.filter(point => {
    point.altitude_km >= minimum_km && point.altitude_km <= maximum_km
  })
}

///|
pub fn track_filter_time(
  track : Array[GroundTrackPoint],
  start_s : Double,
  end_s : Double,
) -> Array[GroundTrackPoint] {
  track.filter(point => point.time_s >= start_s && point.time_s <= end_s)
}

///|
pub fn track_latitude_histogram(
  track : Array[GroundTrackPoint],
  bins : Int,
) -> Histogram {
  histogram(track.map(point => degrees(point.latitude_rad)), -90.0, 90.0, bins)
}

///|
pub fn track_altitude_stats(track : Array[GroundTrackPoint]) -> SampleStats {
  summarize_samples(track.map(point => point.altitude_km))
}

///|
fn append_csv_line(text : String, point : GroundTrackPoint) -> String {
  text +
  "\{point.time_s},\{point.latitude_rad},\{point.longitude_rad},\{point.altitude_km}\n"
}

///|
pub fn track_to_csv(track : Array[GroundTrackPoint]) -> String {
  let mut text = "time_s,latitude_rad,longitude_rad,altitude_km\n"
  for point in track {
    text = append_csv_line(text, point)
  }
  text
}

///|
pub fn segment_ground_track(
  track : Array[GroundTrackPoint],
  maximum_gap_s : Double,
) -> Array[GroundTrackSegment] {
  if track.length() == 0 {
    []
  } else {
    let segments : Array[GroundTrackSegment] = []
    let points : Array[GroundTrackPoint] = [track[0]]
    for i in 1.. maximum_gap_s.max(0.0) {
        segments.push(make_ground_track_segment(points))
        points.clear()
      }
      points.push(track[i])
    }
    if points.length() > 0 {
      segments.push(make_ground_track_segment(points))
    }
    segments
  }
}

///|
fn make_ground_track_segment(
  points : Array[GroundTrackPoint],
) -> GroundTrackSegment {
  let first = points[0]
  let last = points[points.length() - 1]
  {
    points,
    start_time_s: first.time_s,
    end_time_s: last.time_s,
    path_length_km: ground_track_path_length(points),
  }
}

///|
fn track_interpolate(
  a : GroundTrackPoint,
  b : GroundTrackPoint,
  time_s : Double,
) -> GroundTrackPoint {
  let fraction = inverse_lerp(a.time_s, b.time_s, time_s)
  {
    time_s,
    latitude_rad: lerp_scalar(a.latitude_rad, b.latitude_rad, fraction),
    longitude_rad: lerp_scalar(a.longitude_rad, b.longitude_rad, fraction),
    altitude_km: lerp_scalar(a.altitude_km, b.altitude_km, fraction),
  }
}

///|
pub fn resample_ground_track(
  track : Array[GroundTrackPoint],
  times : Array[Double],
) -> Array[GroundTrackPoint] {
  let result : Array[GroundTrackPoint] = []
  for time in times {
    if track.length() == 0 ||
      time < track[0].time_s ||
      time > track[track.length() - 1].time_s {
      continue
    }
    let mut inserted = false
    for i in 0.. SampleStats {
  if track.length() < 2 {
    summarize_samples([])
  } else {
    let steps : Array[Double] = []
    for i in 0..<(track.length() - 1) {
      steps.push(track[i + 1].time_s - track[i].time_s)
    }
    summarize_samples(steps)
  }
}

///|
pub fn propagate_state_samples(
  mu : Double,
  elements : ClassicalElements,
  times_s : Array[Double],
) -> Array[StateSample] {
  times_s.map(time_s => {
    let propagated = propagate_kepler(mu, elements, time_s)
    { time_s, state: elements_to_state(mu, propagated) }
  })
}

///|
pub fn state_sample_at(
  samples : Array[StateSample],
  time_s : Double,
) -> StateSample? {
  for sample in samples {
    if sample.time_s == time_s {
      return Some(sample)
    }
  }
  None
}

///|
fn pipeline_position_error(a : StateVector, b : StateVector) -> Double {
  a.position_km.distance(b.position_km)
}

///|
fn pipeline_velocity_error(a : StateVector, b : StateVector) -> Double {
  a.velocity_km_s.distance(b.velocity_km_s)
}

///|
pub fn compare_state_samples(
  expected : Array[StateSample],
  actual : Array[StateSample],
) -> StateComparison {
  let count = expected.length().min(actual.length())
  if count == 0 {
    {
      count: 0,
      position_rms_km: 0.0,
      velocity_rms_km_s: 0.0,
      position_max_km: 0.0,
      velocity_max_km_s: 0.0,
      time_span_s: 0.0,
    }
  } else {
    let mut position_square = 0.0
    let mut velocity_square = 0.0
    let mut position_max = 0.0
    let mut velocity_max = 0.0
    for i in 0.. Bool {
  comparison.count > 0 &&
  comparison.position_max_km <= position_tolerance_km.abs() &&
  comparison.velocity_max_km_s <= velocity_tolerance_km_s.abs()
}

///|
pub fn state_comparison_score(comparison : StateComparison) -> Double {
  if comparison.count == 0 {
    0.0
  } else {
    1.0 / (1.0 + comparison.position_rms_km + comparison.velocity_rms_km_s)
  }
}

///|
pub fn state_samples_to_csv(samples : Array[StateSample]) -> String {
  let mut text = "time_s,x_km,y_km,z_km,vx_km_s,vy_km_s,vz_km_s\n"
  for sample in samples {
    let state = sample.state
    text = text +
      "\{sample.time_s},\{state.position_km.x},\{state.position_km.y},\{state.position_km.z},\{state.velocity_km_s.x},\{state.velocity_km_s.y},\{state.velocity_km_s.z}\n"
  }
  text
}