///|
pub fn collect_calendar_stats(calendar : Calendar) -> CalendarStats {
  let mut total_components = 0
  let mut total_properties = calendar.properties.length()
  let mut events = 0
  let mut todos = 0
  let mut journals = 0
  let mut timezones = 0
  let mut observances = 0
  let mut alarms = 0
  let mut recurring_components = 0
  let mut components_with_uid = 0
  let mut unknown_properties = count_unknown_calendar_properties(calendar)
  let mut unsupported_properties = count_unsupported_calendar_properties(
    calendar,
  )
  for component in calendar.components {
    total_components += component_total_components(component)
    total_properties += component_total_properties(component)
    events += component_count_by_name(component, "VEVENT")
    todos += component_count_by_name(component, "VTODO")
    journals += component_count_by_name(component, "VJOURNAL")
    timezones += component_count_by_name(component, "VTIMEZONE")
    observances += component_count_by_name(component, "STANDARD")
    observances += component_count_by_name(component, "DAYLIGHT")
    alarms += component_count_by_name(component, "VALARM")
    recurring_components += component_count_recurring(component)
    components_with_uid += component_count_with_uid(component)
    unknown_properties += component_unknown_property_count_deep(component)
    unsupported_properties += component_unsupported_property_count_deep(
      component,
    )
  }
  {
    calendar_properties: calendar.properties.length(),
    top_level_components: calendar.components.length(),
    total_components,
    total_properties,
    events,
    todos,
    journals,
    timezones,
    observances,
    alarms,
    recurring_components,
    components_with_uid,
    unknown_properties,
    unsupported_properties,
  }
}

///|
pub fn event_summaries(calendar : Calendar) -> Array[EventSummary] {
  let summaries : Array[EventSummary] = []
  for event in calendar.events() {
    summaries.push(summarize_event(event))
  }
  summaries
}

///|
pub fn summarize_event(event : Component) -> EventSummary {
  {
    uid: event.get("UID").unwrap_or(""),
    summary: event.get("SUMMARY").unwrap_or(""),
    starts_at: event.get("DTSTART").unwrap_or(""),
    ends_at: event.get("DTEND").unwrap_or(""),
    status: event.get("STATUS").unwrap_or(""),
    location: event.get("LOCATION").unwrap_or(""),
    recurring: event.has("RRULE") || event.has("RDATE"),
    alarm_count: event_alarm_count(event),
  }
}

///|
pub fn render_calendar_report(calendar : Calendar) -> String {
  let stats = collect_calendar_stats(calendar)
  let validation = validate_calendar(calendar, ValidationOptions::strict())
  let b = StringBuilder::new()
  b.write_string("# MoonCalGuard calendar report\n\n")
  b.write_string("## Summary\n\n")
  b.write_string("- accepted: ")
  b.write_string(validation.accepted.to_string())
  b.write_string("\n")
  b.write_string("- components: ")
  b.write_string(stats.total_components.to_string())
  b.write_string("\n")
  b.write_string("- properties: ")
  b.write_string(stats.total_properties.to_string())
  b.write_string("\n")
  b.write_string("- events: ")
  b.write_string(stats.events.to_string())
  b.write_string("\n")
  b.write_string("- todos: ")
  b.write_string(stats.todos.to_string())
  b.write_string("\n")
  b.write_string("- journals: ")
  b.write_string(stats.journals.to_string())
  b.write_string("\n")
  b.write_string("- timezones: ")
  b.write_string(stats.timezones.to_string())
  b.write_string("\n")
  b.write_string("- alarms: ")
  b.write_string(stats.alarms.to_string())
  b.write_string("\n")
  b.write_string("- recurring: ")
  b.write_string(stats.recurring_components.to_string())
  b.write_string("\n\n")
  b.write_string("## Events\n\n")
  let summaries = event_summaries(calendar)
  if summaries.length() == 0 {
    b.write_string("No VEVENT components.\n\n")
  } else {
    for item in summaries {
      b.write_string("- ")
      b.write_string(render_event_summary(item))
      b.write_string("\n")
    }
    b.write_string("\n")
  }
  b.write_string("## Findings\n\n")
  for finding in validation.findings {
    b.write_string("- [")
    b.write_string(finding.severity.name())
    b.write_string("] ")
    b.write_string(finding.code)
    b.write_string(": ")
    b.write_string(finding.message)
    b.write_string("\n")
  }
  b.to_string()
}

///|
pub fn render_calendar_json(calendar : Calendar) -> String {
  let stats = collect_calendar_stats(calendar)
  let validation = validate_calendar(calendar, ValidationOptions::strict())
  let b = StringBuilder::new()
  b.write_string("{")
  b.write_string("\"accepted\":")
  b.write_string(validation.accepted.to_string())
  b.write_string(",\"stats\":")
  b.write_string(render_stats_json(stats))
  b.write_string(",\"events\":[")
  let summaries = event_summaries(calendar)
  let mut i = 0
  while i < summaries.length() {
    if i > 0 {
      b.write_string(",")
    }
    b.write_string(render_event_summary_json(summaries[i]))
    i += 1
  }
  b.write_string("],\"findings\":[")
  let mut j = 0
  while j < validation.findings.length() {
    if j > 0 {
      b.write_string(",")
    }
    b.write_string(render_finding_json(validation.findings[j]))
    j += 1
  }
  b.write_string("]}")
  b.to_string()
}

///|
pub fn render_stats_json(stats : CalendarStats) -> String {
  let b = StringBuilder::new()
  b.write_string("{")
  write_json_int_field(
    b,
    "calendar_properties",
    stats.calendar_properties,
    false,
  )
  write_json_int_field(
    b,
    "top_level_components",
    stats.top_level_components,
    true,
  )
  write_json_int_field(b, "total_components", stats.total_components, true)
  write_json_int_field(b, "total_properties", stats.total_properties, true)
  write_json_int_field(b, "events", stats.events, true)
  write_json_int_field(b, "todos", stats.todos, true)
  write_json_int_field(b, "journals", stats.journals, true)
  write_json_int_field(b, "timezones", stats.timezones, true)
  write_json_int_field(b, "observances", stats.observances, true)
  write_json_int_field(b, "alarms", stats.alarms, true)
  write_json_int_field(
    b,
    "recurring_components",
    stats.recurring_components,
    true,
  )
  write_json_int_field(
    b,
    "components_with_uid",
    stats.components_with_uid,
    true,
  )
  write_json_int_field(b, "unknown_properties", stats.unknown_properties, true)
  write_json_int_field(
    b,
    "unsupported_properties",
    stats.unsupported_properties,
    true,
  )
  b.write_string("}")
  b.to_string()
}

///|
pub fn render_event_summary(summary : EventSummary) -> String {
  let b = StringBuilder::new()
  if summary.uid.length() > 0 {
    b.write_string(summary.uid)
  } else {
    b.write_string("(no uid)")
  }
  if summary.summary.length() > 0 {
    b.write_string(" ")
    b.write_string(unescape_text(summary.summary))
  }
  if summary.starts_at.length() > 0 {
    b.write_string(" start=")
    b.write_string(summary.starts_at)
  }
  if summary.ends_at.length() > 0 {
    b.write_string(" end=")
    b.write_string(summary.ends_at)
  }
  if summary.status.length() > 0 {
    b.write_string(" status=")
    b.write_string(summary.status)
  }
  if summary.location.length() > 0 {
    b.write_string(" location=")
    b.write_string(unescape_text(summary.location))
  }
  if summary.recurring {
    b.write_string(" recurring")
  }
  if summary.alarm_count > 0 {
    b.write_string(" alarms=")
    b.write_string(summary.alarm_count.to_string())
  }
  b.to_string()
}

///|
pub fn render_event_summary_json(summary : EventSummary) -> String {
  let b = StringBuilder::new()
  b.write_string("{")
  write_json_string_field(b, "uid", summary.uid, false)
  write_json_string_field(b, "summary", summary.summary, true)
  write_json_string_field(b, "starts_at", summary.starts_at, true)
  write_json_string_field(b, "ends_at", summary.ends_at, true)
  write_json_string_field(b, "status", summary.status, true)
  write_json_string_field(b, "location", summary.location, true)
  b.write_string(",\"recurring\":")
  b.write_string(summary.recurring.to_string())
  b.write_string(",\"alarm_count\":")
  b.write_string(summary.alarm_count.to_string())
  b.write_string("}")
  b.to_string()
}

///|
pub fn render_finding_json(finding : Finding) -> String {
  let b = StringBuilder::new()
  b.write_string("{")
  write_json_string_field(b, "severity", finding.severity.name(), false)
  write_json_string_field(b, "code", finding.code, true)
  write_json_string_field(b, "message", finding.message, true)
  match finding.line {
    Some(line) => {
      b.write_string(",\"line\":")
      b.write_string(line.to_string())
    }
    None => b.write_string(",\"line\":null")
  }
  b.write_string("}")
  b.to_string()
}

///|
pub fn stats_health_score(stats : CalendarStats) -> Int {
  let mut score = 100
  if stats.events == 0 && stats.todos == 0 && stats.journals == 0 {
    score -= 30
  }
  if stats.unknown_properties > 0 {
    score -= stats.unknown_properties * 2
  }
  if stats.unsupported_properties > 0 {
    score -= stats.unsupported_properties * 3
  }
  if stats.components_with_uid < stats.events + stats.todos + stats.journals {
    score -= 10
  }
  if score < 0 {
    0
  } else {
    score
  }
}

///|
pub fn stats_complexity_label(stats : CalendarStats) -> String {
  if stats.total_components >= 1000 {
    "large"
  } else if stats.total_components >= 100 {
    "medium"
  } else if stats.total_components >= 10 {
    "small"
  } else {
    "tiny"
  }
}

///|
pub fn stats_feature_label(stats : CalendarStats) -> String {
  if stats.recurring_components > 0 && stats.alarms > 0 && stats.timezones > 0 {
    "recurring-alarmed-timezoned"
  } else if stats.recurring_components > 0 && stats.alarms > 0 {
    "recurring-alarmed"
  } else if stats.recurring_components > 0 {
    "recurring"
  } else if stats.alarms > 0 {
    "alarmed"
  } else {
    "basic"
  }
}

///|
pub fn render_stats_line(stats : CalendarStats) -> String {
  let b = StringBuilder::new()
  b.write_string(stats_complexity_label(stats))
  b.write_string(" ")
  b.write_string(stats_feature_label(stats))
  b.write_string(" score=")
  b.write_string(stats_health_score(stats).to_string())
  b.write_string(" events=")
  b.write_string(stats.events.to_string())
  b.write_string(" todos=")
  b.write_string(stats.todos.to_string())
  b.write_string(" alarms=")
  b.write_string(stats.alarms.to_string())
  b.write_string(" recurring=")
  b.write_string(stats.recurring_components.to_string())
  b.to_string()
}

///|
fn component_total_components(component : Component) -> Int {
  let mut total = 1
  for child in component.components {
    total += component_total_components(child)
  }
  total
}

///|
fn component_total_properties(component : Component) -> Int {
  let mut total = component.properties.length()
  for child in component.components {
    total += component_total_properties(child)
  }
  total
}

///|
fn component_count_by_name(component : Component, name : String) -> Int {
  let mut total = if component.name == name { 1 } else { 0 }
  for child in component.components {
    total += component_count_by_name(child, name)
  }
  total
}

///|
fn component_count_recurring(component : Component) -> Int {
  let mut total = if component.has("RRULE") || component.has("RDATE") {
    1
  } else {
    0
  }
  for child in component.components {
    total += component_count_recurring(child)
  }
  total
}

///|
fn component_count_with_uid(component : Component) -> Int {
  let mut total = if component.has("UID") { 1 } else { 0 }
  for child in component.components {
    total += component_count_with_uid(child)
  }
  total
}

///|
fn count_unknown_calendar_properties(calendar : Calendar) -> Int {
  let mut total = 0
  for prop in calendar.properties {
    if !is_known_property(prop.name) {
      total += 1
    }
  }
  total
}

///|
fn count_unsupported_calendar_properties(calendar : Calendar) -> Int {
  let mut total = 0
  for prop in calendar.properties {
    if !property_allowed_in_role(prop.name, CalendarRole) {
      total += 1
    }
  }
  total
}

///|
fn component_unknown_property_count_deep(component : Component) -> Int {
  let mut total = component_unknown_property_count(component)
  for child in component.components {
    total += component_unknown_property_count_deep(child)
  }
  total
}

///|
fn component_unsupported_property_count_deep(component : Component) -> Int {
  let mut total = component_unsupported_property_count(component)
  for child in component.components {
    total += component_unsupported_property_count_deep(child)
  }
  total
}

///|
fn write_json_string_field(
  b : StringBuilder,
  key : String,
  value : String,
  comma : Bool,
) -> Unit {
  if comma {
    b.write_string(",")
  }
  b.write_string("\"")
  b.write_string(key)
  b.write_string("\":\"")
  b.write_string(json_escape(value))
  b.write_string("\"")
}

///|
fn write_json_int_field(
  b : StringBuilder,
  key : String,
  value : Int,
  comma : Bool,
) -> Unit {
  if comma {
    b.write_string(",")
  }
  b.write_string("\"")
  b.write_string(key)
  b.write_string("\":")
  b.write_string(value.to_string())
}

///|
fn json_escape(value : String) -> String {
  let b = StringBuilder::new()
  let mut i = 0
  while i < value.length() {
    let c = value[i]
    if c == 34 {
      b.write_string("\\\"")
    } else if c == 92 {
      b.write_string("\\\\")
    } else if c == 10 {
      b.write_string("\\n")
    } else if c == 13 {
      b.write_string("\\r")
    } else if c == 9 {
      b.write_string("\\t")
    } else {
      b.write_char(c.unsafe_to_char())
    }
    i += 1
  }
  b.to_string()
}