///|
/// One iCalendar component: its kind, its own properties in wire order, and
/// any nested components. Nesting is real (`VALARM` lives inside `VEVENT`,
/// `VEVENT` inside `VCALENDAR`), so the model keeps a tree instead of a flat
/// list, and properties stay in the order the feed sent them.
pub struct Component {
  kind : String
  properties : Array[@text.ContentLine]
  children : Array[Component]
} derive(Debug, Eq)

///|
pub extend Component with @moonbitlang/core/debug.Debug::{to_repr}

///|
pub extend Component with Eq::{not_equal, equal}

///|
/// The first property with this name, if the component carries one.
pub fn Component::property(
  self : Component,
  name : String,
) -> @text.ContentLine? {
  let want = String::to_upper(name)
  let mut found : @text.ContentLine? = None
  for p in self.properties {
    if p.name == want {
      found = Some(p)
      break
    }
  }
  found
}

///|
/// Every property with this name — `EXDATE` and `RDATE` may be repeated, and
/// a single one may hold a comma-separated list.
pub fn Component::properties_named(
  self : Component,
  name : String,
) -> Array[@text.ContentLine] {
  let want = String::to_upper(name)
  let out : Array[@text.ContentLine] = []
  for p in self.properties {
    if p.name == want {
      out.push(p)
    }
  }
  out
}

///|
/// Build a component tree from already-unfolded logical lines.
///
/// Unknown components are preserved rather than dropped, so an unusual feed
/// loses no data. Unbalanced `BEGIN`/`END`, a mismatched `END` name, and a
/// property outside any component all raise [`@text.ParseError`] carrying the
/// offending line number.
///
/// # Example
/// ```mbt nocheck
/// fn test_example() raise {
///   let roots = @model.parse_components([
///     "BEGIN:VCALENDAR", "BEGIN:VEVENT", "SUMMARY:Sync", "END:VEVENT", "END:VCALENDAR",
///   ])
///   assert_eq(roots.length(), 1)
///   assert_eq(roots[0].children[0].properties[0].value, "Sync")
/// }
/// ```
pub fn parse_components(lines : Array[String]) -> Array[Component] raise {
  let roots : Array[Component] = []
  let open : Array[Component] = []
  let total = lines.length()
  for i in 0..
          raise @text.ParseError::BadLine(
            line_no~,
            line=raw,
            message="END without a matching BEGIN",
          )
        Some(done) =>
          if done.kind != line.value.to_upper() {
            raise @text.ParseError::BadLine(
              line_no~,
              line=raw,
              message="END \{line.value} closes \{done.kind}",
            )
          } else if open.length() == 0 {
            roots.push(done)
          } else {
            let last = open.length() - 1
            open[last].children.push(done)
          }
      }
    } else if open.length() == 0 {
      raise @text.ParseError::BadLine(
        line_no~,
        line=raw,
        message="property outside any component",
      )
    } else {
      let last = open.length() - 1
      open[last].properties.push(line)
    }
  }
  if open.length() > 0 {
    let last = open.length() - 1
    raise @text.ParseError::BadLine(
      line_no=lines.length(),
      line="",
      message="unclosed component: \{open[last].kind}",
    )
  }
  roots
}