///|
pub fn event_effective_end(event : Event) -> DateTime? {
  match event.end {
    Some(value) => Some(value)
    None =>
      match event.duration {
        Some(duration) => Some(duration.apply(event.start))
        None =>
          if event.start.is_date {
            Some(event.start.add_days(1))
          } else {
            None
          }
      }
  }
}

///|
pub fn event_is_recurring(event : Event) -> Bool {
  event.rrule is Some(_) || !event.rdate.is_empty()
}

///|
pub fn event_overlaps(event : Event, from : DateTime, until : DateTime) -> Bool {
  let finish = match event_effective_end(event) {
    Some(value) => value
    None => event.start
  }
  !finish.before(from) && !event.start.after(until)
}

///|
pub fn events_between(
  calendar : Calendar,
  from : DateTime,
  until : DateTime,
) -> Array[Event] {
  let result : Array[Event] = []
  for event in calendar.events {
    if event_overlaps(event, from, until) {
      result.push(event)
    }
  }
  result
}

///|
pub fn occurrences_on_date(
  calendar : Calendar,
  date : DateTime,
  limit? : Int = 128,
) -> Result[Array[Occurrence], MoonCalError] {
  let from = DateTime(date.year, date.month, date.day)
  let until = DateTime(
    date.year,
    date.month,
    date.day,
    hour=23,
    minute=59,
    second=59,
  )
  occurrences_between(calendar, from, until, limit~)
}

///|
pub fn next_occurrences(
  calendar : Calendar,
  after : DateTime,
  limit? : Int = 10,
) -> Result[Array[Occurrence], MoonCalError] {
  let horizon = after.add_days(366)
  match occurrences_between(calendar, after, horizon, limit=limit * 4) {
    Ok(items) => {
      let result : Array[Occurrence] = []
      for item in items {
        if !item.start.before(after) && result.length() < limit {
          result.push(item)
        }
      }
      Ok(result)
    }
    Err(err) => Err(err)
  }
}

///|
pub fn expand_calendar_between(
  calendar : Calendar,
  from : DateTime,
  until : DateTime,
  limit? : Int = 256,
) -> Result[Array[Occurrence], MoonCalError] {
  if until.before(from) {
    return Err(
      InvalidCalendar(message="occurrence window end must not be before start"),
    )
  }
  let all : Array[Occurrence] = []
  for event in calendar.events {
    match expand_event_between(event, from, until, limit~) {
      Ok(items) => all.append(items)
      Err(err) => return Err(err)
    }
    if all.length() > limit {
      return Err(LimitExceeded(limit~))
    }
  }
  sort_occurrences(all)
  Ok(all)
}

///|
pub fn expand_event_between(
  event : Event,
  from : DateTime,
  until : DateTime,
  limit? : Int = 256,
) -> Result[Array[Occurrence], MoonCalError] {
  let out : Array[Occurrence] = []
  match event.rrule {
    Some(rule) => {
      let stop = match rule.until {
        Some(rule_until) =>
          if rule_until.before(until) {
            rule_until
          } else {
            until
          }
        None => until
      }
      let mut candidate = event.start
      let mut generated = 0
      let mut steps = 0
      while !candidate.after(stop) && steps < 50000 {
        if rrule_matches_start(rule, event.start, candidate) {
          generated = generated + 1
          if rule.count is Some(max_count) {
            if generated > max_count {
              break
            }
          }
          if !candidate.before(from) &&
            !candidate.after(until) &&
            !contains_datetime(event.exdate, candidate) {
            push_occurrence_once(out, make_occurrence(event, candidate))
          }
        }
        candidate = candidate.add_days(1)
        steps = steps + 1
      }
      if steps >= 50000 {
        return Err(LimitExceeded(limit=50000))
      }
    }
    None =>
      if event_overlaps(event, from, until) &&
        !contains_datetime(event.exdate, event.start) {
        push_occurrence_once(out, make_occurrence(event, event.start))
      }
  }
  for extra in event.rdate {
    if !extra.before(from) &&
      !extra.after(until) &&
      !contains_datetime(event.exdate, extra) {
      push_occurrence_once(out, make_occurrence(event, extra))
    }
  }
  sort_occurrences(out)
  if out.length() > limit {
    Err(LimitExceeded(limit~))
  } else {
    Ok(out)
  }
}

///|
fn make_occurrence(event : Event, start : DateTime) -> Occurrence {
  let end = match event_effective_end(event) {
    Some(finish) => {
      let diff = event.start.seconds_until(finish)
      Some(start.add_seconds(diff))
    }
    None => None
  }
  Occurrence::{ uid: event.uid, start, end, summary: event.summary }
}

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

///|
fn push_occurrence_once(values : Array[Occurrence], item : Occurrence) -> Unit {
  for existing in values {
    if existing.uid == item.uid && existing.start == item.start {
      return
    }
  }
  values.push(item)
}

///|
fn sort_occurrences(values : Array[Occurrence]) -> Unit {
  for i in 1.. 0 && current.start.before(values[j - 1].start) {
      values[j] = values[j - 1]
      j = j - 1
    }
    values[j] = current
  }
}

///|
pub fn task_is_completed(task : Task) -> Bool {
  if task.completed is Some(_) {
    return true
  }
  if task.status is Some(status) {
    if status == "COMPLETED" {
      return true
    }
  }
  match task.percent_complete {
    Some(value) => value >= 100
    None => false
  }
}

///|
pub fn task_is_cancelled(task : Task) -> Bool {
  task.status == Some("CANCELLED")
}

///|
pub fn task_is_in_process(task : Task) -> Bool {
  task.status == Some("IN-PROCESS")
}

///|
pub fn task_is_open(task : Task) -> Bool {
  !task_is_completed(task) && !task_is_cancelled(task)
}

///|
pub fn task_is_overdue(task : Task, as_of : DateTime) -> Bool {
  if !task_is_open(task) {
    return false
  }
  match task.due {
    Some(due) => due.before(as_of)
    None => false
  }
}

///|
pub fn task_due_on_date(task : Task, date : DateTime) -> Bool {
  match task.due {
    Some(due) => due.same_day(date)
    None => false
  }
}

///|
pub fn task_due_between(task : Task, from : DateTime, until : DateTime) -> Bool {
  match task.due {
    Some(due) => !due.before(from) && !due.after(until)
    None => false
  }
}

///|
pub fn find_task(calendar : Calendar, uid : String) -> Task? {
  for task in calendar.tasks {
    if task.uid == uid {
      return Some(task)
    }
  }
  None
}

///|
pub fn open_tasks(calendar : Calendar) -> Array[Task] {
  let result : Array[Task] = []
  for task in calendar.tasks {
    if task_is_open(task) {
      result.push(task)
    }
  }
  sort_tasks_by_due(result)
  result
}

///|
pub fn completed_tasks(calendar : Calendar) -> Array[Task] {
  let result : Array[Task] = []
  for task in calendar.tasks {
    if task_is_completed(task) {
      result.push(task)
    }
  }
  sort_tasks_by_due(result)
  result
}

///|
pub fn cancelled_tasks(calendar : Calendar) -> Array[Task] {
  let result : Array[Task] = []
  for task in calendar.tasks {
    if task_is_cancelled(task) {
      result.push(task)
    }
  }
  sort_tasks_by_due(result)
  result
}

///|
pub fn overdue_tasks(calendar : Calendar, as_of : DateTime) -> Array[Task] {
  let result : Array[Task] = []
  for task in calendar.tasks {
    if task_is_overdue(task, as_of) {
      result.push(task)
    }
  }
  sort_tasks_by_due(result)
  result
}

///|
pub fn tasks_due_between(
  calendar : Calendar,
  from : DateTime,
  until : DateTime,
) -> Array[Task] {
  let result : Array[Task] = []
  for task in calendar.tasks {
    if task_due_between(task, from, until) {
      result.push(task)
    }
  }
  sort_tasks_by_due(result)
  result
}

///|
pub fn tasks_due_on_date(calendar : Calendar, date : DateTime) -> Array[Task] {
  let result : Array[Task] = []
  for task in calendar.tasks {
    if task_due_on_date(task, date) {
      result.push(task)
    }
  }
  sort_tasks_by_due(result)
  result
}

///|
pub fn high_priority_tasks(calendar : Calendar) -> Array[Task] {
  let result : Array[Task] = []
  for task in calendar.tasks {
    if task.priority is Some(value) {
      if value > 0 && value <= 4 {
        result.push(task)
      }
    }
  }
  sort_tasks_by_due(result)
  result
}

///|
pub fn summarize_tasks(calendar : Calendar) -> TaskSummary {
  let mut open_task_count = 0
  let mut completed_task_count = 0
  let mut in_process_task_count = 0
  let mut cancelled_task_count = 0
  let mut high_priority_task_count = 0
  let mut due_task_count = 0
  let mut earliest_due : DateTime? = None
  let mut latest_due : DateTime? = None
  for task in calendar.tasks {
    if task_is_open(task) {
      open_task_count = open_task_count + 1
    }
    if task_is_completed(task) {
      completed_task_count = completed_task_count + 1
    }
    if task_is_in_process(task) {
      in_process_task_count = in_process_task_count + 1
    }
    if task_is_cancelled(task) {
      cancelled_task_count = cancelled_task_count + 1
    }
    if task.priority is Some(value) {
      if value > 0 && value <= 4 {
        high_priority_task_count = high_priority_task_count + 1
      }
    }
    if task.due is Some(due) {
      due_task_count = due_task_count + 1
      earliest_due = min_datetime(earliest_due, due)
      latest_due = max_datetime(latest_due, due)
    }
  }
  TaskSummary::{
    task_count: calendar.tasks.length(),
    open_task_count,
    completed_task_count,
    in_process_task_count,
    cancelled_task_count,
    high_priority_task_count,
    due_task_count,
    earliest_due,
    latest_due,
  }
}

///|
pub fn freebusy_period_overlaps(
  period : FreeBusyPeriod,
  from : DateTime,
  until : DateTime,
) -> Bool {
  !period.end.before(from) && !period.start.after(until)
}

///|
pub fn freebusy_period_contains(
  period : FreeBusyPeriod,
  moment : DateTime,
) -> Bool {
  !moment.before(period.start) && moment.before(period.end)
}

///|
pub fn freebusy_is_busy_type(period : FreeBusyPeriod) -> Bool {
  period.busy_type == "BUSY" ||
  period.busy_type == "BUSY-TENTATIVE" ||
  period.busy_type == "BUSY-UNAVAILABLE"
}

///|
pub fn freebusy_periods_between(
  calendar : Calendar,
  from : DateTime,
  until : DateTime,
) -> Array[FreeBusyPeriod] {
  let result : Array[FreeBusyPeriod] = []
  for item in calendar.freebusy {
    for period in item.periods {
      if freebusy_period_overlaps(period, from, until) {
        result.push(period)
      }
    }
  }
  sort_freebusy_periods(result)
  result
}

///|
pub fn busy_periods_between(
  calendar : Calendar,
  from : DateTime,
  until : DateTime,
) -> Array[FreeBusyPeriod] {
  let result : Array[FreeBusyPeriod] = []
  for period in freebusy_periods_between(calendar, from, until) {
    if freebusy_is_busy_type(period) {
      result.push(period)
    }
  }
  result
}

///|
pub fn free_periods_between(
  calendar : Calendar,
  from : DateTime,
  until : DateTime,
) -> Array[FreeBusyPeriod] {
  let result : Array[FreeBusyPeriod] = []
  for period in freebusy_periods_between(calendar, from, until) {
    if period.busy_type == "FREE" {
      result.push(period)
    }
  }
  result
}

///|
pub fn time_is_busy(calendar : Calendar, moment : DateTime) -> Bool {
  for item in calendar.freebusy {
    for period in item.periods {
      if freebusy_is_busy_type(period) &&
        freebusy_period_contains(period, moment) {
        return true
      }
    }
  }
  false
}

///|
pub fn next_busy_periods(
  calendar : Calendar,
  after : DateTime,
  limit? : Int = 10,
) -> Array[FreeBusyPeriod] {
  let horizon = after.add_days(366)
  let result : Array[FreeBusyPeriod] = []
  for period in busy_periods_between(calendar, after, horizon) {
    if !period.end.before(after) && result.length() < limit {
      result.push(period)
    }
  }
  result
}

///|
pub fn summarize_freebusy(calendar : Calendar) -> FreeBusySummary {
  let mut period_count = 0
  let mut busy_period_count = 0
  let mut tentative_period_count = 0
  let mut unavailable_period_count = 0
  let mut free_period_count = 0
  let mut earliest_start : DateTime? = None
  let mut latest_end : DateTime? = None
  for item in calendar.freebusy {
    for period in item.periods {
      period_count = period_count + 1
      if period.busy_type == "BUSY" {
        busy_period_count = busy_period_count + 1
      } else if period.busy_type == "BUSY-TENTATIVE" {
        tentative_period_count = tentative_period_count + 1
      } else if period.busy_type == "BUSY-UNAVAILABLE" {
        unavailable_period_count = unavailable_period_count + 1
      } else if period.busy_type == "FREE" {
        free_period_count = free_period_count + 1
      }
      earliest_start = min_datetime(earliest_start, period.start)
      latest_end = max_datetime(latest_end, period.end)
    }
  }
  FreeBusySummary::{
    component_count: calendar.freebusy.length(),
    period_count,
    busy_period_count,
    tentative_period_count,
    unavailable_period_count,
    free_period_count,
    earliest_start,
    latest_end,
  }
}

///|
fn sort_tasks_by_due(values : Array[Task]) -> Unit {
  for i in 1.. 0 && task_compare_by_due(current, values[j - 1]) < 0 {
      values[j] = values[j - 1]
      j = j - 1
    }
    values[j] = current
  }
}

///|
fn task_compare_by_due(left : Task, right : Task) -> Int {
  match (left.due, right.due) {
    (Some(a), Some(b)) => a.compare(b)
    (Some(_), None) => -1
    (None, Some(_)) => 1
    (None, None) => left.uid.compare(right.uid)
  }
}

///|
fn sort_freebusy_periods(values : Array[FreeBusyPeriod]) -> Unit {
  for i in 1.. 0 && current.start.before(values[j - 1].start) {
      values[j] = values[j - 1]
      j = j - 1
    }
    values[j] = current
  }
}

///|
fn min_datetime(current : DateTime?, candidate : DateTime) -> DateTime? {
  match current {
    Some(value) =>
      if candidate.before(value) {
        Some(candidate)
      } else {
        Some(value)
      }
    None => Some(candidate)
  }
}

///|
fn max_datetime(current : DateTime?, candidate : DateTime) -> DateTime? {
  match current {
    Some(value) =>
      if candidate.after(value) {
        Some(candidate)
      } else {
        Some(value)
      }
    None => Some(candidate)
  }
}

///|
pub fn summarize(calendar : Calendar) -> CalendarSummary {
  let diagnostics = validate_calendar(calendar)
  let task_summary = summarize_tasks(calendar)
  let freebusy_summary = summarize_freebusy(calendar)
  let mut recurring_event_count = 0
  let mut all_day_event_count = 0
  let mut earliest : DateTime? = None
  let mut latest : DateTime? = None
  for event in calendar.events {
    if event_is_recurring(event) {
      recurring_event_count = recurring_event_count + 1
    }
    if event.start.is_date {
      all_day_event_count = all_day_event_count + 1
    }
    earliest = min_datetime(earliest, event.start)
    latest = max_datetime(latest, event.start)
  }
  for task in calendar.tasks {
    if task.start is Some(start) {
      earliest = min_datetime(earliest, start)
      latest = max_datetime(latest, start)
    }
    if task.due is Some(due) {
      earliest = min_datetime(earliest, due)
      latest = max_datetime(latest, due)
    }
  }
  for item in calendar.freebusy {
    for period in item.periods {
      earliest = min_datetime(earliest, period.start)
      latest = max_datetime(latest, period.end)
    }
  }
  CalendarSummary::{
    event_count: calendar.events.length(),
    recurring_event_count,
    all_day_event_count,
    task_count: task_summary.task_count,
    open_task_count: task_summary.open_task_count,
    completed_task_count: task_summary.completed_task_count,
    blocked_time_count: freebusy_summary.component_count,
    blocked_period_count: freebusy_summary.period_count,
    earliest,
    latest,
    diagnostics,
  }
}

///|
pub fn find_event(calendar : Calendar, uid : String) -> Event? {
  for event in calendar.events {
    if event.uid == uid {
      return Some(event)
    }
  }
  None
}