///|
/// Create a recurring obligation schedule.
pub fn ObligationSchedule::new(
  start~ : Date,
  rule~ : ScheduleRule,
  calendar~ : Calendar,
  extension~ : ExtensionPolicy,
) -> Result[ObligationSchedule, ScheduleError] {
  let valid = match rule {
    EveryCalendarDays(interval) => interval > 0
    EveryBusinessDays(interval) => interval > 0
    WeeklyOn(_, interval) => interval > 0
    MonthlyOn(day, interval) => day >= 1 && day <= 31 && interval > 0
  }
  if !valid {
    Err(ScheduleInvalidInterval)
  } else {
    Ok({ start, rule, calendar, extension })
  }
}

///|
/// Generate a fixed number of occurrences, including the start anchor.
pub fn ObligationSchedule::occurrences(
  self : ObligationSchedule,
  count : Int,
) -> Result[Array[Date], ScheduleError] {
  if count <= 0 {
    return Err(ScheduleInvalidCount)
  }
  let dates = Array::new()
  let mut current = self.start
  match self.rule {
    WeeklyOn(weekday, _) => current = next_weekday(current, weekday)
    _ => ()
  }
  let mut index = 0
  while index < count {
    let mut adjusted = match
      apply_schedule_extension(self.extension, current, self.calendar) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    // Calendar extensions can map adjacent raw occurrences to the same
    // business date (for example Saturday and Sunday both following to
    // Monday). Skip such raw occurrences so the public schedule is strictly
    // increasing and never emits duplicate obligations.
    let mut searching = true
    while searching {
      match dates.last() {
        None => searching = false
        Some(previous) =>
          if adjusted > previous {
            searching = false
          } else {
            current = match next_occurrence(current, self.rule, self.calendar) {
              Ok(value) => value
              Err(error) => return Err(error)
            }
            adjusted = match
              apply_schedule_extension(self.extension, current, self.calendar) {
              Ok(value) => value
              Err(error) => return Err(error)
            }
          }
      }
    }
    dates.push(adjusted)
    if index + 1 == count {
      break
    }
    current = match next_occurrence(current, self.rule, self.calendar) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    index = index + 1
  }
  Ok(dates)
}

///|
fn next_occurrence(
  current : Date,
  rule : ScheduleRule,
  calendar : Calendar,
) -> Result[Date, ScheduleError] {
  match rule {
    EveryCalendarDays(interval) =>
      match current.try_add_days(interval) {
        Ok(value) => Ok(value)
        Err(error) => Err(ScheduleDateFailure(error))
      }
    EveryBusinessDays(interval) =>
      match calendar.advance_business_days(current, interval) {
        Ok(value) => Ok(value)
        Err(error) => Err(ScheduleCalendarFailure(error))
      }
    WeeklyOn(_, interval) =>
      match current.try_add_days(interval * 7) {
        Ok(value) => Ok(value)
        Err(error) => Err(ScheduleDateFailure(error))
      }
    MonthlyOn(day, interval) => add_month_anchor(current, day, interval)
  }
}

///|
fn add_month_anchor(
  current : Date,
  day : Int,
  interval : Int,
) -> Result[Date, ScheduleError] {
  match current.try_add_months(interval) {
    Ok(value) =>
      match
        Date::new(
          year=value.year,
          month=value.month,
          day=Int::min(day, days_in_month(value.year, value.month)),
        ) {
        Ok(value) => Ok(value)
        Err(error) => Err(ScheduleDateFailure(error))
      }
    Err(error) => Err(ScheduleDateFailure(error))
  }
}

///|
fn next_weekday(start : Date, target : Weekday) -> Date {
  let mut current = start
  let mut steps = 0
  while current.weekday() != target && steps < 7 {
    current = current.add_days(1)
    steps = steps + 1
  }
  current
}

///|
fn apply_schedule_extension(
  policy : ExtensionPolicy,
  date : Date,
  calendar : Calendar,
) -> Result[Date, ScheduleError] {
  match policy {
    NoExtension => Ok(date)
    Following =>
      if calendar.is_business_day(date) {
        Ok(date)
      } else {
        match calendar.next_business_day(date) {
          Ok(value) => Ok(value)
          Err(error) => Err(ScheduleCalendarFailure(error))
        }
      }
    Preceding =>
      if calendar.is_business_day(date) {
        Ok(date)
      } else {
        match calendar.previous_business_day(date) {
          Ok(value) => Ok(value)
          Err(error) => Err(ScheduleCalendarFailure(error))
        }
      }
    ModifiedFollowing =>
      if calendar.is_business_day(date) {
        Ok(date)
      } else {
        match calendar.next_business_day(date) {
          Ok(next) =>
            if next.month == date.month {
              Ok(next)
            } else {
              match calendar.previous_business_day(date) {
                Ok(value) => Ok(value)
                Err(error) => Err(ScheduleCalendarFailure(error))
              }
            }
          Err(error) => Err(ScheduleCalendarFailure(error))
        }
      }
  }
}