///|
/// Returns a document element unchanged.
pub fn identity_document_transform(
  element : DocumentElement,
) -> DocumentElement {
  element
}

///|
/// Applies a transform recursively to document elements.
pub fn transform_elements(
  element : DocumentElement,
  transform : (DocumentElement) -> DocumentElement,
) -> DocumentElement {
  transform(
    match element {
      Document(children~, notes~, comments~) =>
        Document(
          children=transform_children(children, transform),
          notes~,
          comments~,
        )
      Paragraph(children~, properties~) =>
        Paragraph(children=transform_children(children, transform), properties~)
      Run(children~, properties~) =>
        Run(children=transform_children(children, transform), properties~)
      Hyperlink(children~, href~, anchor~, target_frame~) =>
        Hyperlink(
          children=transform_children(children, transform),
          href~,
          anchor~,
          target_frame~,
        )
      Table(children~, properties~) =>
        Table(children=transform_children(children, transform), properties~)
      TableRow(children~, is_header~) =>
        TableRow(children=transform_children(children, transform), is_header~)
      TableCell(children~, col_span~, row_span~) =>
        TableCell(
          children=transform_children(children, transform),
          col_span~,
          row_span~,
        )
      _ => element
    },
  )
}

///|
fn transform_children(
  children : Array[DocumentElement],
  transform : (DocumentElement) -> DocumentElement,
) -> Array[DocumentElement] {
  let transformed : Array[DocumentElement] = []
  for child in children {
    transformed.push(transform_elements(child, transform))
  }
  transformed
}

///|
/// Builds a transform that targets paragraph elements.
pub fn transform_paragraphs(
  transform : (DocumentElement) -> DocumentElement,
) -> (DocumentElement) -> DocumentElement {
  fn(element) {
    transform_elements(element, fn(element) {
      match element {
        Paragraph(..) => transform(element)
        _ => element
      }
    })
  }
}

///|
/// Builds a transform that targets run elements.
pub fn transform_runs(
  transform : (DocumentElement) -> DocumentElement,
) -> (DocumentElement) -> DocumentElement {
  fn(element) {
    transform_elements(element, fn(element) {
      match element {
        Run(..) => transform(element)
        _ => element
      }
    })
  }
}

///|
/// Returns all descendants of a document element.
pub fn document_descendants(
  element : DocumentElement,
) -> Array[DocumentElement] {
  document_descendants_where(element, fn(_) { true })
}

///|
/// Returns descendants that satisfy a predicate.
pub fn document_descendants_where(
  element : DocumentElement,
  predicate : (DocumentElement) -> Bool,
) -> Array[DocumentElement] {
  let descendants : Array[DocumentElement] = []
  visit_descendants(element, predicate, descendants)
  descendants
}

///|
fn visit_descendants(
  element : DocumentElement,
  predicate : (DocumentElement) -> Bool,
  descendants : Array[DocumentElement],
) -> Unit {
  for child in element_children(element) {
    visit_descendants(child, predicate, descendants)
    if predicate(child) {
      descendants.push(child)
    }
  }
}

///|
fn element_children(element : DocumentElement) -> Array[DocumentElement] {
  match element {
    Document(children~, ..) => children
    Paragraph(children~, ..) => children
    Run(children~, ..) => children
    Hyperlink(children~, ..) => children
    Table(children~, ..) => children
    TableRow(children~, ..) => children
    TableCell(children~, ..) => children
    _ => []
  }
}