///|
/// A day at one station, and the report that comes out of it.
///
/// A monitoring station reports concentrations, not indices. Between the two
/// sits the work the standards actually describe: average the day's hours
/// into the indicators a daily report is made of — twenty-four hours for
/// everything but ozone, and the day's highest eight-hour average for ozone —
/// then turn each indicator into a sub-index and take the worst.
///
/// `Hourly` keeps the hours; this keeps a station's worth of them and does
/// the averaging, so that a caller who has a day of raw readings never has to
/// decide what a partly missing window means.

///|
/// How a report averages its hours.
///
/// Both windows are rules rather than numbers because both are the
/// authority's to set: what counts as enough of a day, and enough of an
/// eight-hour stretch, has been revised more than once in both of the
/// standards this library carries.
pub struct DayRules {
  daily : Window
  ozone : Window
}

///|
/// The shape a report is usually built to: a day averaged over `daily` hours,
/// and ozone judged over its highest `ozone`-hour average.
pub fn DayRules::new(daily : Window, ozone : Window) -> DayRules {
  { daily, ozone, }
}

///|
/// The rule the day's other pollutants are averaged under.
pub fn DayRules::daily(self : DayRules) -> Window {
  self.daily
}

///|
/// The rule ozone is judged over.
pub fn DayRules::ozone(self : DayRules) -> Window {
  self.ozone
}

///|
/// A station's day: the hours recorded for each pollutant.
pub struct Day {
  station : String
  hours : Array[Hourly]
}

///|
/// An empty day at a station.
pub fn Day::new(station : String) -> Day {
  { station, hours: [], }
}

///|
/// The station the day is from.
pub fn Day::station(self : Day) -> String {
  self.station
}

///|
/// One entry per pollutant that has been recorded, in the order the
/// pollutants were first seen.
pub fn Day::hours(self : Day) -> Array[Hourly] {
  self.hours
}

///|
/// The hours recorded for one pollutant, when it has any.
pub fn Day::hourly(self : Day, pollutant : Pollutant) -> Hourly? {
  for hours in self.hours {
    if hours.pollutant() == pollutant {
      return Some(hours)
    }
  }
  None
}

///|
/// Record one hour of one pollutant, starting that pollutant's series the
/// first time it is seen.
///
/// An hour is recorded for every pollutant on every pass, so that the series
/// stay aligned and a gap in one pollutant cannot silently shift another's
/// hours. `None` records an hour without a reading.
pub fn Day::record(self : Day, pollutant : Pollutant, value : Double?) -> Unit {
  match self.hourly(pollutant) {
    Some(hours) => hours.push(value)
    None => {
      let hours = Hourly::new(pollutant)
      hours.push(value)
      self.hours.push(hours)
    }
  }
}

///|
/// The day's indicators, ready to be turned into an index: the average of the
/// day's hours for every pollutant but ozone, and the day's highest eight-hour
/// average for ozone.
///
/// A pollutant whose hours were too thin to average under the rules is left
/// out rather than guessed at, which is what the standards mean by a missing
/// indicator.
pub fn Day::readings(self : Day, rules : DayRules) -> Array[Reading] {
  let readings : Array[Reading] = []
  for hours in self.hours {
    let indicator = match hours.pollutant() {
      O3 => hours.maximum_rolling(rules.ozone())
      _ => hours.mean(rules.daily().minimum_valid())
    }
    match indicator {
      Some(value) => readings.push(Reading::of(hours.pollutant(), Daily, value))
      None => ()
    }
  }
  readings
}

///|
/// The day's index under a standard's tables.
pub fn Day::aqi(
  self : Day,
  standard : Standard,
  rules : DayRules,
) -> AirQuality? {
  aqi_with(standard, self.readings(rules))
}

///|
/// The latest hour of every pollutant, which is what a real-time report is
/// made of.
pub fn Day::latest_readings(self : Day) -> Array[Reading] {
  let readings : Array[Reading] = []
  for hours in self.hours {
    if hours.length() > 0 {
      match hours.at(hours.length() - 1) {
        Some(value) =>
          readings.push(Reading::of(hours.pollutant(), RealTime, value))
        None => ()
      }
    }
  }
  readings
}

///|
/// A city's day, from its stations': the average, hour by hour, of the
/// stations that reported that hour.
///
/// A station that did not report an hour does not drag the city's hour
/// towards zero, and a pollutant no station recorded does not appear. The
/// result is a day like any other and is reported with the same rules.
pub fn Day::merged(station : String, stations : Array[Day]) -> Day {
  let merged = Day::new(station)
  if stations.length() == 0 {
    return merged
  }
  // How many hours the longest station recorded, so that every pollutant of
  // the merged day covers the same span.
  let mut span = 0
  for day in stations {
    for hours in day.hours() {
      if hours.length() > span {
        span = hours.length()
      }
    }
  }
  let pollutants : Array[Pollutant] = []
  for day in stations {
    for hours in day.hours() {
      let mut seen = false
      for known in pollutants {
        if known == hours.pollutant() {
          seen = true
        }
      }
      if !seen {
        pollutants.push(hours.pollutant())
      }
    }
  }
  for pollutant in pollutants {
    for hour = 0; hour < span; hour = hour + 1 {
      let mut total = 0.0
      let mut reported = 0
      for day in stations {
        match day.hourly(pollutant) {
          Some(hours) =>
            if hour < hours.length() {
              match hours.at(hour) {
                Some(value) => {
                  total = total + value
                  reported += 1
                }
                None => ()
              }
            }
          None => ()
        }
      }
      merged.record(
        pollutant,
        if reported == 0 {
          None
        } else {
          Some(total / reported.to_double())
        },
      )
    }
  }
  merged
}