///|
pub fn SuspensionSet::normalize(self : SuspensionSet) -> SuspensionSet {
  let sorted = self.periods.copy()
  sorted.sort_by((left, right) => {
    if left.start < right.start {
      -1
    } else if left.start > right.start {
      1
    } else if left.end < right.end {
      -1
    } else if left.end > right.end {
      1
    } else {
      0
    }
  })
  let merged = Array::new()
  for period in sorted {
    if merged.is_empty() {
      merged.push(period)
    } else {
      let last_index = merged.length() - 1
      let last = merged[last_index]
      match
        DateRange::closed(start=last.start, end=last.end)
        .unwrap()
        .union(DateRange::closed(start=period.start, end=period.end).unwrap()) {
        Ok(union) => merged[last_index] = { start: union.start, end: union.end }
        Err(NoIntersection) => merged.push(period)
        Err(_) => panic()
      }
    }
  }
  { periods: merged.copy(), policy: self.policy, abort_on: self.abort_on }
}

///|
pub fn SuspensionSet::calendar_days(self : SuspensionSet) -> Int {
  let normalized = self.normalize()
  normalized.periods.fold(init=0, (count, period) => {
    count + period_count_calendar_days(period, normalized.policy)
  })
}

///|
pub fn SuspensionSet::business_days(
  self : SuspensionSet,
  calendar : Calendar,
) -> Int {
  let normalized = self.normalize()
  normalized.periods.fold(init=0, (count, period) => {
    count + period_count_business_days(period, normalized.policy, calendar)
  })
}

///|
fn period_count_calendar_days(
  period : SuspensionPeriod,
  policy : SuspensionPolicy,
) -> Int {
  match counted_range_for_period(period, policy) {
    Some(range) => range.calendar_days()
    None => 0
  }
}

///|
fn period_count_business_days(
  period : SuspensionPeriod,
  policy : SuspensionPolicy,
  calendar : Calendar,
) -> Int {
  match counted_range_for_period(period, policy) {
    Some(range) => range.business_days(calendar)
    None => 0
  }
}

///|
fn counted_range_for_period(
  period : SuspensionPeriod,
  policy : SuspensionPolicy,
) -> DateRange? {
  match policy {
    PauseInclusive =>
      Some(DateRange::closed(start=period.start, end=period.end).unwrap())
    PauseExclusive =>
      if period.start == period.end {
        None
      } else {
        let start = period.start.try_add_days(1).unwrap()
        let end = period.end.try_add_days(-1).unwrap()
        if start > end {
          None
        } else {
          Some(DateRange::closed(start~, end~).unwrap())
        }
      }
  }
}

///|
fn suspension_days_in_window(
  suspension : SuspensionSet,
  start : Date,
  end : Date,
) -> Int {
  if start > end {
    return 0
  }
  let window = DateRange::closed(start~, end~).unwrap()
  let normalized = suspension.normalize()
  normalized.periods.fold(init=0, (count, period) => {
    count + overlap_calendar_days(period, window, normalized.policy)
  })
}

///|
fn suspension_business_days_in_window(
  suspension : SuspensionSet,
  start : Date,
  end : Date,
  calendar : Calendar,
) -> Int {
  if start > end {
    return 0
  }
  let window = DateRange::closed(start~, end~).unwrap()
  let normalized = suspension.normalize()
  normalized.periods.fold(init=0, (count, period) => {
    count + overlap_business_days(period, window, normalized.policy, calendar)
  })
}

///|
fn overlap_calendar_days(
  period : SuspensionPeriod,
  window : DateRange,
  policy : SuspensionPolicy,
) -> Int {
  match counted_range_for_period(period, policy) {
    Some(range) =>
      match range.intersection(window) {
        Ok(intersection) => intersection.calendar_days()
        Err(NoIntersection) => 0
        Err(_) => panic()
      }
    None => 0
  }
}

///|
fn overlap_business_days(
  period : SuspensionPeriod,
  window : DateRange,
  policy : SuspensionPolicy,
  calendar : Calendar,
) -> Int {
  match counted_range_for_period(period, policy) {
    Some(range) =>
      match range.intersection(window) {
        Ok(intersection) => intersection.business_days(calendar)
        Err(NoIntersection) => 0
        Err(_) => panic()
      }
    None => 0
  }
}