///|
/// Planning a route against a whole forecast.
///
/// `Route::plan` answers what one level at one airspeed costs. A flight plan
/// asks two more questions: which of the forecast levels is worth flying, and
/// what the tank has to carry to fly it. Both are arithmetic over the legs a
/// plan already carries, so they live here rather than in the triangle.

///|
/// What a plan burns.
pub struct FuelPlan {
  trip_hours : Double
  trip_fuel : Double
  reserve_fuel : Double
}

///|
/// The fuel for a plan at `burn_per_hour`, with a reserve expressed in
/// minutes of flying.
pub fn RoutePlan::fuel(
  self : RoutePlan,
  burn_per_hour : Double,
  reserve_minutes : Double,
) -> FuelPlan {
  let hours = self.total_time_hours()
  {
    trip_hours: hours,
    trip_fuel: hours * burn_per_hour,
    reserve_fuel: reserve_minutes / 60.0 * burn_per_hour,
  }
}

///|
/// The time the plan takes, in hours.
pub fn FuelPlan::trip_hours(self : FuelPlan) -> Double {
  self.trip_hours
}

///|
/// The fuel the flight itself burns.
pub fn FuelPlan::trip_fuel(self : FuelPlan) -> Double {
  self.trip_fuel
}

///|
/// The fuel the reserve needs.
pub fn FuelPlan::reserve_fuel(self : FuelPlan) -> Double {
  self.reserve_fuel
}

///|
/// Trip fuel and reserve together, which is what has to be aboard.
pub fn FuelPlan::total_fuel(self : FuelPlan) -> Double {
  self.trip_fuel + self.reserve_fuel
}

///|
/// The time the same route would take in still air: the distance at the
/// plan's airspeed, with no wind at all.
pub fn RoutePlan::still_air_time_hours(self : RoutePlan) -> Double {
  self.total_distance() / self.airspeed()
}

///|
/// What the wind is worth on this route, in hours: positive when the flight
/// takes longer than it would in still air, negative when the wind pays.
pub fn RoutePlan::wind_effect_hours(self : RoutePlan) -> Double {
  self.total_time_hours() - self.still_air_time_hours()
}

///|
/// The route planned at every level the station forecasts, the fastest level
/// first.
///
/// A level the station does not forecast, or one whose wind leaves no ground
/// speed, is left out rather than failing the whole study — a level that
/// cannot be flown is not an answer, but it is not a reason to stop asking
/// about the others.
pub fn Route::plans_at_levels(
  self : Route,
  station : StationWinds,
  airspeed : Double,
) -> Array[RoutePlan] {
  let plans : Array[RoutePlan] = []
  for level in station.levels() {
    let planned = Some(self.plan(station, level.altitude(), airspeed)) catch {
      _ => None
    }
    match planned {
      Some(plan) => plans.push(plan)
      None => ()
    }
  }
  sort_by_total_time(plans)
  plans
}

///|
/// The fastest level the station forecasts, or `None` when none of them can
/// be flown.
pub fn Route::fastest_level(
  self : Route,
  station : StationWinds,
  airspeed : Double,
) -> RoutePlan? {
  let plans = self.plans_at_levels(station, airspeed)
  if plans.length() == 0 {
    None
  } else {
    Some(plans[0])
  }
}

///|
/// Put the plans in order of total time, fastest first.
fn sort_by_total_time(plans : Array[RoutePlan]) -> Unit {
  for i = 1; i < plans.length(); i = i + 1 {
    let current = plans[i]
    let mut j = i
    while j > 0 && plans[j - 1].total_time_hours() > current.total_time_hours() {
      plans[j] = plans[j - 1]
      j -= 1
    }
    plans[j] = current
  }
}