///|
pub fn parse_calendar(input : String) -> Result[Calendar, CalError] {
  Ok(parse_calendar_or_raise(input)) catch {
    err => Err(err)
  }
}

///|
fn parse_calendar_or_raise(input : String) -> Calendar raise CalError {
  if trim_ascii(input).length() == 0 {
    raise EmptyInput
  }
  let lines = unfold_lines(input)
  let calendar : Calendar = { properties: [], components: [] }
  let mut in_calendar = false
  let mut calendar_started = false
  let mut calendar_closed = false
  let stack : Array[Component] = []
  let mut line_no = 1
  for raw in lines {
    let prop = parse_content_line(raw, line_no)
    if prop.name == "BEGIN" {
      let component_name = ascii_upper(trim_ascii(prop.value))
      if component_name == "VCALENDAR" {
        if in_calendar {
          raise NestedCalendar
        }
        if calendar_closed {
          raise MultipleCalendars
        }
        calendar_started = true
        in_calendar = true
      } else {
        if !in_calendar {
          raise ComponentOutsideCalendar(component_name)
        }
        stack.push({
          name: component_name,
          properties: [],
          components: [],
          line: prop.line,
        })
      }
    } else if prop.name == "END" {
      let component_name = ascii_upper(trim_ascii(prop.value))
      if component_name == "VCALENDAR" {
        if !in_calendar {
          raise UnexpectedEnd(component_name)
        }
        if stack.length() > 0 {
          raise MismatchedEnd(stack[stack.length() - 1].name, "VCALENDAR")
        }
        in_calendar = false
        calendar_closed = true
      } else {
        if stack.length() == 0 {
          raise UnexpectedEnd(component_name)
        }
        let active_index = stack.length() - 1
        let active = stack[active_index]
        if active.name != component_name {
          raise MismatchedEnd(active.name, component_name)
        }
        let completed = stack[active_index]
        ignore(stack.pop())
        if stack.length() == 0 {
          calendar.components.push(completed)
        } else {
          let parent_index = stack.length() - 1
          stack[parent_index].components.push(completed)
        }
      }
    } else {
      if !in_calendar {
        raise PropertyOutsideCalendar(prop.name)
      }
      if stack.length() == 0 {
        calendar.properties.push(prop)
      } else {
        let active_index = stack.length() - 1
        stack[active_index].properties.push(prop)
      }
    }
    line_no += 1
  }
  if in_calendar {
    if stack.length() == 0 {
      raise MismatchedEnd("VCALENDAR", "EOF")
    }
    raise MismatchedEnd(stack[stack.length() - 1].name, "EOF")
  }
  if !calendar_started {
    raise MissingCalendar
  }
  calendar
}

///|
pub fn parse_content_line(raw : String, line : Int) -> Property raise CalError {
  let colon = index_of_char(raw, 58)
  if colon < 0 {
    raise MissingColon(line, raw)
  }
  let head = raw[0:colon].to_owned()
  let value = raw[colon + 1:raw.length()].to_owned()
  let parts = split_quoted(head, 59)
  if parts.length() == 0 {
    raise BadPropertyName(line, raw)
  }
  let name = ascii_upper(trim_ascii(parts[0]))
  if !valid_token(name) {
    raise BadPropertyName(line, raw)
  }
  let params : Array[Param] = []
  let mut i = 1
  while i < parts.length() {
    params.push(parse_param(parts[i], line))
    i += 1
  }
  { name, params, value, line }
}

///|
fn parse_param(raw : String, line : Int) -> Param raise CalError {
  let eq = index_of_char(raw, 61)
  if eq <= 0 {
    raise BadParameter(line, raw)
  }
  let name = ascii_upper(trim_ascii(raw[0:eq].to_owned()))
  if !valid_token(name) {
    raise BadParameter(line, raw)
  }
  let value_raw = raw[eq + 1:raw.length()].to_owned()
  let values = split_quoted(value_raw, 44)
  let cleaned : Array[String] = []
  for value in values {
    cleaned.push(strip_quotes(trim_ascii(value)))
  }
  { name, values: cleaned }
}

///|
fn strip_quotes(s : String) -> String {
  if s.length() >= 2 && s[0] == 34 && s[s.length() - 1] == 34 {
    s[1:s.length() - 1].to_owned()
  } else {
    s
  }
}

///|
pub fn Calendar::events(self : Calendar) -> Array[Component] {
  let events : Array[Component] = []
  for component in self.components {
    if component.name == "VEVENT" {
      events.push(component)
    }
  }
  events
}

///|
pub fn Component::children(self : Component, name : String) -> Array[Component] {
  let wanted = ascii_upper(name)
  let children : Array[Component] = []
  for component in self.components {
    if component.name == wanted {
      children.push(component)
    }
  }
  children
}

///|
pub fn Component::alarms(self : Component) -> Array[Component] {
  self.children("VALARM")
}

///|
pub fn Component::get(self : Component, name : String) -> String? {
  let wanted = ascii_upper(name)
  for prop in self.properties {
    if prop.name == wanted {
      return Some(prop.value)
    }
  }
  None
}

///|
pub fn Component::properties_named(
  self : Component,
  name : String,
) -> Array[Property] {
  let wanted = ascii_upper(name)
  let properties : Array[Property] = []
  for prop in self.properties {
    if prop.name == wanted {
      properties.push(prop)
    }
  }
  properties
}

///|
pub fn Calendar::get(self : Calendar, name : String) -> String? {
  let wanted = ascii_upper(name)
  for prop in self.properties {
    if prop.name == wanted {
      return Some(prop.value)
    }
  }
  None
}

///|
pub fn Calendar::properties_named(
  self : Calendar,
  name : String,
) -> Array[Property] {
  let wanted = ascii_upper(name)
  let properties : Array[Property] = []
  for prop in self.properties {
    if prop.name == wanted {
      properties.push(prop)
    }
  }
  properties
}

///|
pub fn Component::has(self : Component, name : String) -> Bool {
  self.get(name) is Some(_)
}

///|
pub fn Component::count(self : Component, name : String) -> Int {
  let wanted = ascii_upper(name)
  let mut count = 0
  for prop in self.properties {
    if prop.name == wanted {
      count += 1
    }
  }
  count
}

///|
pub fn Component::param_value(
  self : Component,
  property : String,
  param : String,
) -> String? {
  let wanted_property = ascii_upper(property)
  let wanted_param = ascii_upper(param)
  for prop in self.properties {
    if prop.name == wanted_property {
      match prop.param_value(wanted_param) {
        Some(value) => return Some(value)
        None => ()
      }
    }
  }
  None
}

///|
pub fn Property::param_value(self : Property, name : String) -> String? {
  let wanted = ascii_upper(name)
  for param in self.params {
    if param.name == wanted && param.values.length() > 0 {
      return Some(param.values[0])
    }
  }
  None
}