///|
// Calendar candidate generation is shared by numeric locators and naive rule
// wrappers. Only numeric consumers need ordinal clipping, UTC heaps or epochs.
priv struct RecurrenceCalendarWindow {
  first_date : DateTime
  last_date : DateTime
  anchor : DateTime
}

///|
fn recurrence_candidates(
  rule : DateRule,
  window : RecurrenceCalendarWindow,
) -> Iter[Int64] {
  match rule.frequency {
    Hourly => recurrence_clock_candidates(rule, window, 3600)
    Minutely => recurrence_clock_candidates(rule, window, 60)
    Secondly => recurrence_clock_candidates(rule, window, 1)
    _ => recurrence_calendar_candidates(rule, window)
  }
}

///|
fn recurrence_week_start(value : DateTime, wkst : Int32) -> Int32 {
  days_from_civil(value.year, value.month, value.day) -
  (value.weekday() - wkst + 7) % 7
}

///|
// Index periods in calendar units, so interval phase survives variable month
// lengths and leap years. Weeks use the explicitly configured first weekday.
fn recurrence_calendar_index(value : DateTime, rule : DateRule) -> Int32 {
  match rule.frequency {
    Yearly => value.year
    Monthly => value.year * 12 + value.month - 1
    Weekly => recurrence_week_start(value, rule.wkst)
    _ => days_from_civil(value.year, value.month, value.day)
  }
}

///|
fn recurrence_period_days(
  index : Int32,
  frequency : RRuleFrequency,
) -> (Int32, Int32) {
  match frequency {
    Yearly => (days_from_civil(index, 1, 1), days_from_civil(index + 1, 1, 1))
    Monthly => {
      let year = index / 12
      let month = index % 12 + 1
      let start = days_from_civil(year, month, 1)
      (start, start + days_in_month(year, month))
    }
    Weekly => (index, index + 7)
    _ => (index, index + 1)
  }
}

///|
fn recurrence_calendar_candidates(
  rule : DateRule,
  window : RecurrenceCalendarWindow,
) -> Iter[Int64] {
  let period_days = if rule.frequency is Weekly { 7L } else { 1L }
  let step = Int64::from_int(rule.interval) * period_days
  let anchor_index = Int64::from_int(
    recurrence_calendar_index(window.anchor, rule),
  )
  let first_index = Int64::from_int(
    if rule.count is Some(_) {
      Int32::min(
        recurrence_calendar_index(window.first_date, rule),
        recurrence_calendar_index(window.anchor, rule),
      )
    } else {
      recurrence_calendar_index(window.first_date, rule)
    },
  )
  let last_index = Int64::from_int(
    recurrence_calendar_index(window.last_date, rule),
  )
  let start = locator_align(first_index, anchor_index, step)
  let times = recurrence_times(rule, window.anchor)
  let mut index = start
  let mut current : Iter[Int64] = Iter::empty()
  Iter::new(fn() {
    while true {
      match current.next() {
        Some(value) => return Some(value)
        None => ()
      }
      if index > last_index {
        return None
      }
      let (first_day, end_day) = recurrence_period_days(
        index.to_int(),
        rule.frequency,
      )
      index += step
      let days = recurrence_days(rule, window.anchor, first_day, end_day)
      current = match rule.bysetpos {
        Some(positions) =>
          recurrence_indices(positions, days.length() * times.length())
          .iter()
          .map(fn(position) {
            Int64::from_int(days[position / times.length()]) * 86400L +
            Int64::from_int(times[position % times.length()])
          })
        None =>
          days
          .iter()
          .flat_map(day => {
            times
            .iter()
            .map(time => Int64::from_int(day) * 86400L + Int64::from_int(time))
          })
      }
    }
    None
  })
}