///|
pub suberror ICalendarError {
  MissingStart
  InvalidContentLine(String)
  UnsupportedProperty(String)
  InvalidDateValue(String)
} derive(Eq, Debug)

///|
fn unfold_ical_lines(source : String) -> Array[String] {
  let normalized = source.replace_all(old="\r\n", new="\n")
  let result : Array[String] = []
  for line_view in normalized.split("\n") {
    let line = line_view.to_owned()
    if line.length() == 0 {
      continue
    }
    if (line.has_prefix(" ") || line.has_prefix("\t")) && result.length() > 0 {
      let previous = result[result.length() - 1]
      result[result.length() - 1] = previous + line[1:].to_owned()
    } else {
      result.push(line)
    }
  }
  result
}

///|
fn property_name(left : String) -> String {
  let parts : Array[String] = left
    .split(";")
    .map(value => value.to_owned())
    .collect()
  parts[0].to_upper()
}

///|
fn parse_datetime_list(value : String) -> Array[DateTime] raise ICalendarError {
  let result : Array[DateTime] = []
  for item in value.split(",") {
    let text = item.to_owned()
    let without_z = if text.has_suffix("Z") {
      text[:text.length() - 1].to_owned()
    } else {
      text
    }
    let parsed = parse_datetime(without_z) catch {
      _ => raise InvalidDateValue(text)
    }
    result.push(parsed)
  }
  result
}

///|
/// Parse the recurrence-related subset of an iCalendar component.
pub fn parse_ical_recurrence(source : String) -> RecurrenceSet raise {
  let mut start : DateTime? = None
  let rules : Array[RRule] = []
  let included : Array[DateTime] = []
  let excluded : Array[DateTime] = []
  for line in unfold_ical_lines(source) {
    if line.has_prefix("BEGIN:") ||
      line.has_prefix("END:") ||
      line.has_prefix("VERSION:") ||
      line.has_prefix("PRODID:") ||
      line.has_prefix("CALSCALE:") ||
      line.has_prefix("METHOD:") {
      continue
    }
    let split = line.split_once(":")
    let (left_view, value_view) = match split {
      Some(pair) => pair
      None => raise InvalidContentLine(line)
    }
    let left = left_view.to_owned()
    let value = value_view.to_owned()
    match property_name(left) {
      "DTSTART" => {
        let values = parse_datetime_list(value)
        if values.length() != 1 {
          raise InvalidDateValue(value)
        }
        start = Some(values[0])
      }
      "RRULE" => rules.push(parse_rrule(value))
      "RDATE" =>
        for item in parse_datetime_list(value) {
          included.push(item)
        }
      "EXDATE" =>
        for item in parse_datetime_list(value) {
          excluded.push(item)
        }
      "UID" | "SUMMARY" | "DESCRIPTION" | "DTSTAMP" | "SEQUENCE" | "STATUS" =>
        ()
      name => raise UnsupportedProperty(name)
    }
  }
  let actual_start = match start {
    Some(value) => value
    None => raise MissingStart
  }
  {
    start: actual_start,
    rules,
    include_dates: included,
    exclude_dates: excluded,
  }
}

///|
fn basic_datetime(value : DateTime) -> String {
  pad4(value.date.year) +
  pad2(value.date.month) +
  pad2(value.date.day) +
  "T" +
  pad2(value.time.hour) +
  pad2(value.time.minute) +
  pad2(value.time.second)
}

///|
fn fold_content_line(line : String) -> Array[String] {
  if line.length() <= 75 {
    return [line]
  }
  let result : Array[String] = []
  let mut offset = 0
  let mut first = true
  while offset < line.length() {
    let width = if first { 75 } else { 74 }
    let end = (offset + width).min(line.length())
    let part = line[offset:end].to_owned()
    result.push(if first { part } else { " " + part })
    first = false
    offset = end
  }
  result
}

///|
pub fn RecurrenceSet::to_ical(
  self : RecurrenceSet,
  uid? : String = "rrule-lab@example",
) -> String {
  let logical : Array[String] = [
    "BEGIN:VCALENDAR",
    "VERSION:2.0",
    "PRODID:-//RRuleLab//MoonBit RFC5545 Engine//EN",
    "BEGIN:VEVENT",
    "UID:" + uid,
    "DTSTART:" + basic_datetime(self.start),
  ]
  for rule in self.rules {
    logical.push("RRULE:" + rule.to_string())
  }
  if self.include_dates.length() > 0 {
    logical.push(
      "RDATE:" +
      self.include_dates.map(value => basic_datetime(value)).join(","),
    )
  }
  if self.exclude_dates.length() > 0 {
    logical.push(
      "EXDATE:" +
      self.exclude_dates.map(value => basic_datetime(value)).join(","),
    )
  }
  logical.push("END:VEVENT")
  logical.push("END:VCALENDAR")
  let physical : Array[String] = []
  for line in logical {
    for folded in fold_content_line(line) {
      physical.push(folded)
    }
  }
  physical.join("\r\n") + "\r\n"
}