///|
pub fn parse_recurrence_rule(
  input : String,
) -> Result[RecurrenceRule, RecurrenceError] {
  Ok(parse_recurrence_rule_or_raise(input)) catch {
    err => Err(err)
  }
}

///|
fn parse_recurrence_rule_or_raise(
  input : String,
) -> RecurrenceRule raise RecurrenceError {
  let raw = trim_ascii(input)
  if raw.length() == 0 {
    raise EmptyRule
  }
  let parts = split_by_char(raw, 59)
  let seen : Array[String] = []
  let by_day : Array[ByDayRule] = []
  let by_month : Array[Int] = []
  let by_month_day : Array[Int] = []
  let by_set_pos : Array[Int] = []
  let mut frequency = Daily
  let mut has_frequency = false
  let mut interval = 1
  let mut count : Int? = None
  let mut until : String? = None
  let mut wkst : Weekday? = None
  for part in parts {
    let eq = index_of_char(part, 61)
    if eq <= 0 {
      raise UnknownPart(part)
    }
    let key = ascii_upper(trim_ascii(part[0:eq].to_owned()))
    let value = trim_ascii(part[eq + 1:part.length()].to_owned())
    if contains_string(seen, key) {
      raise DuplicatePart(key)
    }
    seen.push(key)
    if key == "FREQ" {
      frequency = parse_frequency(value)
      has_frequency = true
    } else if key == "INTERVAL" {
      interval = parse_positive_int(value)
    } else if key == "COUNT" {
      count = Some(parse_positive_int(value))
    } else if key == "UNTIL" {
      until = Some(parse_until_value(value))
    } else if key == "WKST" {
      wkst = Some(parse_weekday(value))
    } else if key == "BYDAY" {
      parse_by_day_list(value, by_day)
    } else if key == "BYMONTH" {
      parse_int_list(value, by_month, 1, 12, false)
    } else if key == "BYMONTHDAY" {
      parse_int_list(value, by_month_day, -31, 31, true)
    } else if key == "BYSETPOS" {
      parse_int_list(value, by_set_pos, -366, 366, true)
    } else {
      raise UnknownPart(key)
    }
  }
  if !has_frequency {
    raise MissingFrequency
  }
  match (count, until) {
    (Some(_), Some(_)) => raise ConflictingLimit
    _ => ()
  }
  {
    frequency,
    interval,
    count,
    until,
    wkst,
    by_day,
    by_month,
    by_month_day,
    by_set_pos,
  }
}

///|
pub fn render_recurrence_rule(rule : RecurrenceRule) -> String {
  let b = StringBuilder::new()
  b.write_string("FREQ=")
  b.write_string(rule.frequency.name())
  if rule.interval != 1 {
    b.write_string(";INTERVAL=")
    b.write_string(rule.interval.to_string())
  }
  match rule.count {
    Some(count) => {
      b.write_string(";COUNT=")
      b.write_string(count.to_string())
    }
    None => ()
  }
  match rule.until {
    Some(until) => {
      b.write_string(";UNTIL=")
      b.write_string(until)
    }
    None => ()
  }
  match rule.wkst {
    Some(day) => {
      b.write_string(";WKST=")
      b.write_string(day.abbr())
    }
    None => ()
  }
  if rule.by_day.length() > 0 {
    b.write_string(";BYDAY=")
    b.write_string(render_by_day_list(rule.by_day))
  }
  if rule.by_month.length() > 0 {
    b.write_string(";BYMONTH=")
    b.write_string(render_int_list(rule.by_month))
  }
  if rule.by_month_day.length() > 0 {
    b.write_string(";BYMONTHDAY=")
    b.write_string(render_int_list(rule.by_month_day))
  }
  if rule.by_set_pos.length() > 0 {
    b.write_string(";BYSETPOS=")
    b.write_string(render_int_list(rule.by_set_pos))
  }
  b.to_string()
}

///|
pub fn normalize_recurrence_rule(
  input : String,
) -> Result[String, RecurrenceError] {
  match parse_recurrence_rule(input) {
    Ok(rule) => Ok(render_recurrence_rule(rule))
    Err(err) => Err(err)
  }
}

///|
pub fn RecurrenceRule::is_finite(self : RecurrenceRule) -> Bool {
  self.count is Some(_) || self.until is Some(_)
}

///|
pub fn RecurrenceRule::has_by_rules(self : RecurrenceRule) -> Bool {
  self.by_day.length() > 0 ||
  self.by_month.length() > 0 ||
  self.by_month_day.length() > 0 ||
  self.by_set_pos.length() > 0
}

///|
pub fn RecurrenceRule::uses_weekday(
  self : RecurrenceRule,
  day : Weekday,
) -> Bool {
  for item in self.by_day {
    if item.weekday == day {
      return true
    }
  }
  false
}

///|
pub fn RecurrenceRule::uses_month(self : RecurrenceRule, month : Int) -> Bool {
  for item in self.by_month {
    if item == month {
      return true
    }
  }
  false
}

///|
pub fn RecurrenceRule::uses_month_day(self : RecurrenceRule, day : Int) -> Bool {
  for item in self.by_month_day {
    if item == day {
      return true
    }
  }
  false
}

///|
pub fn RecurrenceRule::limit_description(self : RecurrenceRule) -> String {
  match self.count {
    Some(count) => "COUNT=\{count}"
    None =>
      match self.until {
        Some(until) => "UNTIL=\{until}"
        None => "unbounded"
      }
  }
}

///|
pub fn RecurrenceRule::frequency_score(self : RecurrenceRule) -> Int {
  match self.frequency {
    Secondly => 0
    Minutely => 1
    Hourly => 2
    Daily => 3
    Weekly => 4
    Monthly => 5
    Yearly => 6
  }
}

///|
pub fn RecurrenceRule::density_hint(self : RecurrenceRule) -> String {
  match self.frequency {
    Secondly => "very-high"
    Minutely => "high"
    Hourly => "medium-high"
    Daily => "medium"
    Weekly => "medium-low"
    Monthly => "low"
    Yearly => "very-low"
  }
}

///|
pub fn RecurrenceRule::shape_label(self : RecurrenceRule) -> String {
  let b = StringBuilder::new()
  b.write_string(self.frequency.name())
  if self.interval != 1 {
    b.write_string("/")
    b.write_string(self.interval.to_string())
  }
  if self.by_day.length() > 0 {
    b.write_string(" days=")
    b.write_string(render_by_day_list(self.by_day))
  }
  if self.by_month.length() > 0 {
    b.write_string(" months=")
    b.write_string(render_int_list(self.by_month))
  }
  if self.by_month_day.length() > 0 {
    b.write_string(" monthdays=")
    b.write_string(render_int_list(self.by_month_day))
  }
  b.to_string()
}

///|
pub fn RecurrenceRule::estimated_upper_bound(self : RecurrenceRule) -> Int? {
  match self.count {
    Some(count) => Some(count)
    None =>
      match self.until {
        Some(_) => Some(10000)
        None => None
      }
  }
}

///|
pub fn RecurrenceRule::looks_dense_for_feed(self : RecurrenceRule) -> Bool {
  if self.frequency_score() <= 2 {
    return true
  }
  match self.count {
    Some(count) => count > 5000
    None => self.until is None && self.frequency_score() <= 3
  }
}

///|
pub fn recurrence_rule_from_parts(
  frequency : RecurrenceFrequency,
  interval : Int,
  count : Int?,
  until : String?,
) -> RecurrenceRule {
  {
    frequency,
    interval,
    count,
    until,
    wkst: None,
    by_day: [],
    by_month: [],
    by_month_day: [],
    by_set_pos: [],
  }
}

///|
pub fn daily_rule(count : Int) -> RecurrenceRule {
  recurrence_rule_from_parts(Daily, 1, Some(count), None)
}

///|
pub fn weekly_rule(count : Int, days : Array[Weekday]) -> RecurrenceRule {
  let by_day : Array[ByDayRule] = []
  for day in days {
    by_day.push({ ordinal: None, weekday: day })
  }
  {
    frequency: Weekly,
    interval: 1,
    count: Some(count),
    until: None,
    wkst: None,
    by_day,
    by_month: [],
    by_month_day: [],
    by_set_pos: [],
  }
}

///|
pub fn monthly_nth_weekday_rule(
  count : Int,
  ordinal : Int,
  weekday : Weekday,
) -> RecurrenceRule {
  {
    frequency: Monthly,
    interval: 1,
    count: Some(count),
    until: None,
    wkst: None,
    by_day: [{ ordinal: Some(ordinal), weekday }],
    by_month: [],
    by_month_day: [],
    by_set_pos: [],
  }
}

///|
fn parse_frequency(value : String) -> RecurrenceFrequency raise RecurrenceError {
  let normalized = ascii_upper(trim_ascii(value))
  if normalized == "SECONDLY" {
    Secondly
  } else if normalized == "MINUTELY" {
    Minutely
  } else if normalized == "HOURLY" {
    Hourly
  } else if normalized == "DAILY" {
    Daily
  } else if normalized == "WEEKLY" {
    Weekly
  } else if normalized == "MONTHLY" {
    Monthly
  } else if normalized == "YEARLY" {
    Yearly
  } else {
    raise BadFrequency(value)
  }
}

///|
fn parse_weekday(value : String) -> Weekday raise RecurrenceError {
  let normalized = ascii_upper(trim_ascii(value))
  if normalized == "MO" {
    Monday
  } else if normalized == "TU" {
    Tuesday
  } else if normalized == "WE" {
    Wednesday
  } else if normalized == "TH" {
    Thursday
  } else if normalized == "FR" {
    Friday
  } else if normalized == "SA" {
    Saturday
  } else if normalized == "SU" {
    Sunday
  } else {
    raise BadWeekday(value)
  }
}

///|
fn parse_until_value(value : String) -> String raise RecurrenceError {
  let normalized = trim_ascii(value)
  if is_date(normalized) || is_datetime(normalized) {
    normalized
  } else {
    raise BadUntil(value)
  }
}

///|
fn parse_int_list(
  value : String,
  out : Array[Int],
  min : Int,
  max : Int,
  reject_zero : Bool,
) -> Unit raise RecurrenceError {
  let parts = split_by_char(value, 44)
  for raw in parts {
    let n = parse_signed_int(raw)
    if n < min || n > max || (reject_zero && n == 0) {
      raise BadInteger(raw)
    }
    out.push(n)
  }
}

///|
fn parse_by_day_list(
  value : String,
  out : Array[ByDayRule],
) -> Unit raise RecurrenceError {
  let parts = split_by_char(value, 44)
  for raw in parts {
    out.push(parse_by_day(raw))
  }
}

///|
fn parse_by_day(raw : String) -> ByDayRule raise RecurrenceError {
  let value = ascii_upper(trim_ascii(raw))
  if value.length() < 2 {
    raise BadByDay(raw)
  }
  let day_text = value[value.length() - 2:value.length()].to_owned()
  let weekday = parse_weekday(day_text)
  let prefix = value[0:value.length() - 2].to_owned()
  if prefix.length() == 0 {
    { ordinal: None, weekday }
  } else {
    let ordinal = parse_signed_int(prefix)
    if ordinal == 0 || ordinal < -53 || ordinal > 53 {
      raise BadByDay(raw)
    }
    { ordinal: Some(ordinal), weekday }
  }
}

///|
fn parse_positive_int(raw : String) -> Int raise RecurrenceError {
  let n = parse_signed_int(raw)
  if n <= 0 {
    raise BadInteger(raw)
  }
  n
}

///|
fn parse_signed_int(raw : String) -> Int raise RecurrenceError {
  let value = trim_ascii(raw)
  if value.length() == 0 {
    raise BadInteger(raw)
  }
  let mut sign = 1
  let mut i = 0
  if value[0] == 45 {
    sign = -1
    i = 1
  } else if value[0] == 43 {
    i = 1
  }
  if i >= value.length() {
    raise BadInteger(raw)
  }
  let mut total = 0
  while i < value.length() {
    let code = value[i].to_int()
    if code < 48 || code > 57 {
      raise BadInteger(raw)
    }
    total = total * 10 + code - 48
    i += 1
  }
  total * sign
}

///|
fn render_by_day_list(days : Array[ByDayRule]) -> String {
  let b = StringBuilder::new()
  let mut i = 0
  while i < days.length() {
    if i > 0 {
      b.write_string(",")
    }
    match days[i].ordinal {
      Some(ordinal) => b.write_string(ordinal.to_string())
      None => ()
    }
    b.write_string(days[i].weekday.abbr())
    i += 1
  }
  b.to_string()
}

///|
fn render_int_list(values : Array[Int]) -> String {
  let b = StringBuilder::new()
  let mut i = 0
  while i < values.length() {
    if i > 0 {
      b.write_string(",")
    }
    b.write_string(values[i].to_string())
    i += 1
  }
  b.to_string()
}

///|
fn contains_string(values : Array[String], target : String) -> Bool {
  for value in values {
    if value == target {
      return true
    }
  }
  false
}