///|
pub fn find_event_by_uid(calendar : Calendar, uid : String) -> Component? {
  for event in calendar.events() {
    match event.get("UID") {
      Some(value) => if value == uid { return Some(event) }
      None => ()
    }
  }
  None
}

///|
pub fn calendar_has_uid(calendar : Calendar, uid : String) -> Bool {
  find_event_by_uid(calendar, uid) is Some(_)
}

///|
pub fn events_on_date(calendar : Calendar, date : String) -> Array[Component] {
  let out : Array[Component] = []
  for event in calendar.events() {
    if event_starts_on(event, date) {
      out.push(event)
    }
  }
  out
}

///|
pub fn events_between_dates(
  calendar : Calendar,
  start_date : String,
  end_date : String,
) -> Array[Component] {
  let out : Array[Component] = []
  for event in calendar.events() {
    match event_start_date(event) {
      Some(date) =>
        if date >= start_date && date <= end_date {
          out.push(event)
        }
      None => ()
    }
  }
  out
}

///|
pub fn events_with_status(
  calendar : Calendar,
  status : String,
) -> Array[Component] {
  let out : Array[Component] = []
  let wanted = ascii_upper(trim_ascii(status))
  for event in calendar.events() {
    if event_status(event) == wanted {
      out.push(event)
    }
  }
  out
}

///|
pub fn events_with_alarms(calendar : Calendar) -> Array[Component] {
  let out : Array[Component] = []
  for event in calendar.events() {
    if event_has_alarms(event) {
      out.push(event)
    }
  }
  out
}

///|
pub fn recurring_events(calendar : Calendar) -> Array[Component] {
  let out : Array[Component] = []
  for event in calendar.events() {
    if event_is_recurring(event) {
      out.push(event)
    }
  }
  out
}

///|
pub fn all_day_events(calendar : Calendar) -> Array[Component] {
  let out : Array[Component] = []
  for event in calendar.events() {
    if event_is_all_day(event) {
      out.push(event)
    }
  }
  out
}

///|
pub fn timed_events(calendar : Calendar) -> Array[Component] {
  let out : Array[Component] = []
  for event in calendar.events() {
    if event_is_timed(event) {
      out.push(event)
    }
  }
  out
}

///|
pub fn cancelled_events(calendar : Calendar) -> Array[Component] {
  events_with_status(calendar, "CANCELLED")
}

///|
pub fn confirmed_events(calendar : Calendar) -> Array[Component] {
  events_with_status(calendar, "CONFIRMED")
}

///|
pub fn tentative_events(calendar : Calendar) -> Array[Component] {
  events_with_status(calendar, "TENTATIVE")
}

///|
pub fn events_with_category(
  calendar : Calendar,
  category : String,
) -> Array[Component] {
  let out : Array[Component] = []
  let wanted = ascii_upper(trim_ascii(category))
  for event in calendar.events() {
    if event_has_category(event, wanted) {
      out.push(event)
    }
  }
  out
}

///|
pub fn event_has_category(event : Component, category : String) -> Bool {
  let wanted = ascii_upper(trim_ascii(category))
  for prop in event.properties_named("CATEGORIES") {
    let values = split_by_char(prop.value, 44)
    for value in values {
      if ascii_upper(trim_ascii(value)) == wanted {
        return true
      }
    }
  }
  false
}

///|
pub fn event_is_recurring(event : Component) -> Bool {
  event.has("RRULE") || event.has("RDATE")
}

///|
pub fn event_is_all_day(event : Component) -> Bool {
  match event.get("DTSTART") {
    Some(value) => is_date(value)
    None => false
  }
}

///|
pub fn event_is_timed(event : Component) -> Bool {
  match event.get("DTSTART") {
    Some(value) => is_datetime(value)
    None => false
  }
}

///|
pub fn event_is_cancelled(event : Component) -> Bool {
  event_status(event) == "CANCELLED"
}

///|
pub fn event_is_confirmed(event : Component) -> Bool {
  event_status(event) == "CONFIRMED"
}

///|
pub fn event_is_tentative(event : Component) -> Bool {
  event_status(event) == "TENTATIVE"
}

///|
pub fn event_status(event : Component) -> String {
  ascii_upper(trim_ascii(event.get("STATUS").unwrap_or("")))
}

///|
pub fn event_summary_text(event : Component) -> String {
  unescape_text(event.get("SUMMARY").unwrap_or(""))
}

///|
pub fn event_location_text(event : Component) -> String {
  unescape_text(event.get("LOCATION").unwrap_or(""))
}

///|
pub fn event_start_value(event : Component) -> String {
  event.get("DTSTART").unwrap_or("")
}

///|
pub fn event_end_value(event : Component) -> String {
  event.get("DTEND").unwrap_or("")
}

///|
pub fn event_start_date(event : Component) -> String? {
  let start = event_start_value(event)
  if start.length() >= 8 {
    Some(start[0:8].to_owned())
  } else {
    None
  }
}

///|
pub fn event_starts_on(event : Component, date : String) -> Bool {
  match event_start_date(event) {
    Some(value) => value == date
    None => false
  }
}

///|
pub fn event_timezone(event : Component) -> String? {
  match event.param_value("DTSTART", "TZID") {
    Some(value) => Some(value)
    None => event.param_value("DTEND", "TZID")
  }
}

///|
pub fn event_duration_text(event : Component) -> String {
  match event.get("DURATION") {
    Some(value) => value
    None =>
      if event_start_value(event).length() > 0 &&
        event_end_value(event).length() > 0 {
        event_start_value(event) + ".." + event_end_value(event)
      } else {
        ""
      }
  }
}

///|
pub fn event_primary_rule(event : Component) -> RecurrenceRule? {
  match event.get("RRULE") {
    Some(value) =>
      match parse_recurrence_rule(value) {
        Ok(rule) => Some(rule)
        Err(_) => None
      }
    None => None
  }
}

///|
pub fn event_recurrence_label(event : Component) -> String {
  match event_primary_rule(event) {
    Some(rule) => rule.shape_label()
    None => if event.has("RDATE") { "RDATE" } else { "" }
  }
}

///|
pub fn event_sort_key(event : Component) -> String {
  let b = StringBuilder::new()
  b.write_string(event_start_value(event))
  b.write_string("|")
  b.write_string(event.get("UID").unwrap_or(""))
  b.write_string("|")
  b.write_string(event_summary_text(event))
  b.to_string()
}

///|
pub fn event_matches_text(event : Component, needle : String) -> Bool {
  let query = ascii_upper(trim_ascii(needle))
  if query.length() == 0 {
    return true
  }
  string_contains_ascii_upper(event_summary_text(event), query) ||
  string_contains_ascii_upper(event_location_text(event), query) ||
  string_contains_ascii_upper(event.get("UID").unwrap_or(""), query) ||
  string_contains_ascii_upper(event.get("DESCRIPTION").unwrap_or(""), query)
}

///|
pub fn search_events(calendar : Calendar, needle : String) -> Array[Component] {
  let out : Array[Component] = []
  for event in calendar.events() {
    if event_matches_text(event, needle) {
      out.push(event)
    }
  }
  out
}

///|
pub fn duplicate_event_uids(calendar : Calendar) -> Array[String] {
  let duplicates : Array[String] = []
  let seen : Array[String] = []
  for event in calendar.events() {
    match event.get("UID") {
      Some(uid) =>
        if contains_query_string(seen, uid) {
          if !contains_query_string(duplicates, uid) {
            duplicates.push(uid)
          }
        } else {
          seen.push(uid)
        }
      None => ()
    }
  }
  duplicates
}

///|
pub fn calendar_has_duplicate_event_uids(calendar : Calendar) -> Bool {
  duplicate_event_uids(calendar).length() > 0
}

///|
pub fn render_agenda(calendar : Calendar) -> String {
  let b = StringBuilder::new()
  b.write_string("# Agenda\n\n")
  let events = calendar.events()
  if events.length() == 0 {
    b.write_string("No events.\n")
    return b.to_string()
  }
  for event in events {
    b.write_string("- ")
    b.write_string(render_agenda_event(event))
    b.write_string("\n")
  }
  b.to_string()
}

///|
pub fn render_agenda_for_date(calendar : Calendar, date : String) -> String {
  let b = StringBuilder::new()
  b.write_string("# Agenda ")
  b.write_string(date)
  b.write_string("\n\n")
  let events = events_on_date(calendar, date)
  if events.length() == 0 {
    b.write_string("No events.\n")
    return b.to_string()
  }
  for event in events {
    b.write_string("- ")
    b.write_string(render_agenda_event(event))
    b.write_string("\n")
  }
  b.to_string()
}

///|
pub fn render_agenda_event(event : Component) -> String {
  let b = StringBuilder::new()
  let start = event_start_value(event)
  if start.length() > 0 {
    b.write_string(start)
    b.write_string(" ")
  }
  let end = event_end_value(event)
  if end.length() > 0 {
    b.write_string("to ")
    b.write_string(end)
    b.write_string(" ")
  }
  let summary = event_summary_text(event)
  if summary.length() > 0 {
    b.write_string(summary)
  } else {
    b.write_string("(untitled)")
  }
  let location = event_location_text(event)
  if location.length() > 0 {
    b.write_string(" @ ")
    b.write_string(location)
  }
  let status = event_status(event)
  if status.length() > 0 {
    b.write_string(" [")
    b.write_string(status)
    b.write_string("]")
  }
  if event_is_recurring(event) {
    b.write_string(" ")
    b.write_string(event_recurrence_label(event))
  }
  if event_has_alarms(event) {
    b.write_string(" alarms=")
    b.write_string(event_alarm_count(event).to_string())
  }
  b.to_string()
}

///|
pub fn calendar_status_count(calendar : Calendar, status : String) -> Int {
  events_with_status(calendar, status).length()
}

///|
pub fn calendar_alarm_coverage_percent(calendar : Calendar) -> Int {
  let events = calendar.events()
  if events.length() == 0 {
    return 0
  }
  events_with_alarms(calendar).length() * 100 / events.length()
}

///|
pub fn calendar_recurrence_coverage_percent(calendar : Calendar) -> Int {
  let events = calendar.events()
  if events.length() == 0 {
    return 0
  }
  recurring_events(calendar).length() * 100 / events.length()
}

///|
fn string_contains_ascii_upper(value : String, query : String) -> Bool {
  let haystack = ascii_upper(value)
  if query.length() == 0 {
    return true
  }
  if haystack.length() < query.length() {
    return false
  }
  let mut i = 0
  while i + query.length() <= haystack.length() {
    if haystack[i:i + query.length()].to_owned() == query {
      return true
    }
    i += 1
  }
  false
}

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