///|
/// A calendar recurrence set combines one or more rules and explicit dates,
/// then removes exception dates. This mirrors RFC 5545 recurrence semantics.
pub struct RecurrenceSet {
  start : DateTime
  rules : Array[RRule]
  include_dates : Array[DateTime]
  exclude_dates : Array[DateTime]
} derive(Eq, Debug)

///|
pub fn RecurrenceSet::new(start : DateTime) -> RecurrenceSet {
  { start, rules: [], include_dates: [], exclude_dates: [] }
}

///|
pub fn RecurrenceSet::add_rule(
  self : RecurrenceSet,
  rule : RRule,
) -> RecurrenceSet {
  let rules = self.rules.copy()
  rules.push(rule)
  { ..self, rules, }
}

///|
pub fn RecurrenceSet::add_date(
  self : RecurrenceSet,
  value : DateTime,
) -> RecurrenceSet {
  let include_dates = self.include_dates.copy()
  include_dates.push(value)
  { ..self, include_dates, }
}

///|
pub fn RecurrenceSet::exclude_date(
  self : RecurrenceSet,
  value : DateTime,
) -> RecurrenceSet {
  let exclude_dates = self.exclude_dates.copy()
  exclude_dates.push(value)
  { ..self, exclude_dates, }
}

///|
fn append_unique_datetime(target : Array[DateTime], value : DateTime) -> Unit {
  if !target.contains(value) {
    target.push(value)
  }
}

///|
pub fn RecurrenceSet::expand(
  self : RecurrenceSet,
  options : ExpansionOptions,
) -> Array[DateTime] raise ExpansionError {
  let result : Array[DateTime] = []
  if self.rules.length() == 0 && datetime_in_window(self.start, options) {
    append_unique_datetime(result, self.start)
  }
  for rule in self.rules {
    for value in expand_rrule(self.start, rule, options) {
      append_unique_datetime(result, value)
    }
  }
  for value in self.include_dates {
    if datetime_in_window(value, options) {
      append_unique_datetime(result, value)
    }
  }
  let filtered : Array[DateTime] = []
  result.sort()
  for value in result {
    if !self.exclude_dates.contains(value) && filtered.length() < options.limit {
      filtered.push(value)
    }
  }
  filtered
}

///|
pub(all) enum SetOperation {
  Union
  Intersection
  Difference
} derive(Eq, Debug)

///|
pub fn combine_occurrences(
  left : Array[DateTime],
  right : Array[DateTime],
  operation : SetOperation,
) -> Array[DateTime] {
  let result : Array[DateTime] = []
  match operation {
    Union => {
      for value in left {
        append_unique_datetime(result, value)
      }
      for value in right {
        append_unique_datetime(result, value)
      }
    }
    Intersection =>
      for value in left {
        if right.contains(value) {
          append_unique_datetime(result, value)
        }
      }
    Difference =>
      for value in left {
        if !right.contains(value) {
          append_unique_datetime(result, value)
        }
      }
  }
  result.sort()
  result
}

///|
pub struct RuleDifference {
  only_left : Array[DateTime]
  only_right : Array[DateTime]
  shared : Array[DateTime]
} derive(Eq, Debug)

///|
pub fn compare_rules(
  start : DateTime,
  left : RRule,
  right : RRule,
  options : ExpansionOptions,
) -> RuleDifference raise ExpansionError {
  let left_values = expand_rrule(start, left, options)
  let right_values = expand_rrule(start, right, options)
  {
    only_left: combine_occurrences(left_values, right_values, Difference),
    only_right: combine_occurrences(right_values, left_values, Difference),
    shared: combine_occurrences(left_values, right_values, Intersection),
  }
}