///|
pub enum Frequency {
  Daily
  Weekly
  Monthly
  Yearly
} derive(Eq, Debug)

///|
pub struct RRule {
  frequency : Frequency
  interval : Int
  count : Int?
  until : DateTime?
  weekdays : Array[Int]
  monthdays : Array[Int]
  months : Array[Int]
} derive(Debug)

///|
/// Parse the UTC-safe RFC 5545 subset used by MoonSchedule.
pub fn RRule::parse(input : String) -> RRule raise ScheduleError {
  let mut frequency : Frequency? = None
  let mut interval = 1
  let mut count : Int? = None
  let mut until : DateTime? = None
  let mut weekdays : Array[Int] = []
  let mut monthdays : Array[Int] = []
  let mut months : Array[Int] = []
  for raw in input.trim().split(";") {
    let pair = raw.split("=").to_array()
    if pair.length() != 2 {
      raise InvalidRule("RRULE entries need KEY=VALUE")
    }
    let key = pair[0].to_owned().to_upper()
    let value = pair[1].to_owned().to_upper()
    match key {
      "FREQ" => frequency = Some(parse_frequency(value))
      "INTERVAL" => interval = positive_number(value, "INTERVAL")
      "COUNT" => count = Some(positive_number(value, "COUNT"))
      "UNTIL" => until = Some(DateTime::parse(value))
      "BYDAY" => weekdays = parse_weekdays(value)
      "BYMONTHDAY" => monthdays = parse_numbers(value, 1, 31, "BYMONTHDAY")
      "BYMONTH" => months = parse_numbers(value, 1, 12, "BYMONTH")
      _ => raise InvalidRule("unsupported RRULE key: \{key}")
    }
  }
  match frequency {
    Some(frequency) =>
      { frequency, interval, count, until, weekdays, monthdays, months }
    None => raise InvalidRule("RRULE requires FREQ")
  }
}

///|
/// Enumerate occurrences in `[start, end]`; `start` is the recurrence anchor.
pub fn RRule::occurrences_between(
  self : RRule,
  start : DateTime,
  end : DateTime,
) -> Array[DateTime] {
  let output : Array[DateTime] = []
  let mut candidate = start
  let mut emitted = 0
  for _ in 0..<20000 {
    if candidate > end {
      break
    }
    if self.until is Some(limit) && candidate > limit {
      break
    }
    if self.matches_filters(candidate) {
      output.push(candidate)
      emitted = emitted + 1
      if self.count is Some(limit) && emitted >= limit {
        break
      }
    }
    candidate = advance(candidate, self.frequency, self.interval)
  }
  output
}

///|
pub fn RRule::next_after(self : RRule, after : DateTime) -> DateTime? {
  let end = after.add_minutes(1_100_000)
  let values = self.occurrences_between(after.add_minutes(1), end)
  values.get(0)
}

///|
fn RRule::matches_filters(self : RRule, candidate : DateTime) -> Bool {
  (self.weekdays.length() == 0 || self.weekdays.contains(candidate.weekday())) &&
  (self.monthdays.length() == 0 || self.monthdays.contains(candidate.day)) &&
  (self.months.length() == 0 || self.months.contains(candidate.month))
}

///|
fn advance(
  current : DateTime,
  frequency : Frequency,
  interval : Int,
) -> DateTime {
  match frequency {
    Daily => current.add_minutes(interval * 1440)
    Weekly => current.add_minutes(interval * 10080)
    Monthly => add_months(current, interval)
    Yearly => add_months(current, interval * 12)
  }
}

///|
fn add_months(current : DateTime, amount : Int) -> DateTime {
  let raw = current.year * 12 + current.month - 1 + amount
  let year = raw / 12
  let month = raw % 12 + 1
  let day = if current.day > days_in_month(year, month) {
    days_in_month(year, month)
  } else {
    current.day
  }
  { year, month, day, hour: current.hour, minute: current.minute }
}

///|
fn parse_frequency(value : String) -> Frequency raise ScheduleError {
  match value {
    "DAILY" => Daily
    "WEEKLY" => Weekly
    "MONTHLY" => Monthly
    "YEARLY" => Yearly
    _ => raise InvalidRule("unsupported FREQ: \{value}")
  }
}

///|
fn positive_number(value : String, label : String) -> Int raise ScheduleError {
  let result = @string.parse_int(value) catch {
    _ => raise InvalidRule("\{label} must be a number")
  }
  if result < 1 {
    raise InvalidRule("\{label} must be positive")
  }
  result
}

///|
fn parse_numbers(
  value : String,
  minimum : Int,
  maximum : Int,
  label : String,
) -> Array[Int] raise ScheduleError {
  let output : Array[Int] = []
  for item in value.split(",") {
    let number = positive_number(item.to_owned(), label)
    if number < minimum || number > maximum {
      raise InvalidRule("\{label} value is out of range")
    }
    output.push(number)
  }
  output
}

///|
fn parse_weekdays(value : String) -> Array[Int] raise ScheduleError {
  let output : Array[Int] = []
  for item in value.split(",") {
    let weekday = match item.to_owned().to_upper() {
      "SU" => 0
      "MO" => 1
      "TU" => 2
      "WE" => 3
      "TH" => 4
      "FR" => 5
      "SA" => 6
      _ => raise InvalidRule("BYDAY needs SU,MO,TU,WE,TH,FR,SA")
    }
    output.push(weekday)
  }
  output
}