///|
fn write_payload_line(
  out : StringBuilder,
  prefix : String,
  text : StringView,
  eol : String,
) -> Unit {
  out.write_string(prefix)
  if !text.is_empty() {
    out.write_char(' ')
    if text.has_prefix("@") {
      out.write_char('@')
    }
    out.write_view(text)
  }
  out.write_string(eol)
}

///|
/// Canonical text round-trip preserves payloads and structure order, not bytes.
/// CONT is emitted before ordinary children. Original line endings normalize.
/// bom=true emits the initial UTF-8 signature recommended by GEDCOM 7 ยง1.1.
pub fn serialize(
  doc : Document,
  line_ending~ : String = "\n",
  bom~ : Bool = false,
) -> String raise GedError {
  if line_ending != "\n" && line_ending != "\r\n" && line_ending != "\r" {
    raise Query("line ending must be LF, CRLF or CR")
  }
  let out = StringBuilder()
  if bom {
    out.write_char('\u{FEFF}')
  }
  for node in doc.nodes {
    let label = match node.xref {
      Some(id) => "@" + id + "@ " + node.tag
      None => node.tag
    }
    let prefix = node.level.to_string() + " " + label
    match node.payload {
      Empty => {
        out.write_string(prefix)
        out.write_string(line_ending)
      }
      Pointer(id) => {
        out.write_string(prefix + " @" + id + "@")
        out.write_string(line_ending)
      }
      Void => {
        out.write_string(prefix + " @VOID@")
        out.write_string(line_ending)
      }
      Text(text) => {
        let mut first = true
        for part in text.split("\n") {
          let current = if first {
            prefix
          } else {
            (node.level + 1).to_string() + " CONT"
          }
          write_payload_line(out, current, part, line_ending)
          first = false
        }
      }
    }
  }
  out.to_string()
}

///|
pub fn serialize_bytes(
  doc : Document,
  line_ending~ : String = "\n",
  bom~ : Bool = false,
) -> Bytes raise GedError {
  @utf8.encode(serialize(doc, line_ending~, bom~))
}

///|
/// Equality ignores source line numbers and equivalent empty payload spellings.
pub fn equivalent(a : Document, b : Document) -> Bool {
  if a.nodes.length() != b.nodes.length() {
    return false
  }
  for i, x in a.nodes {
    let y = b.nodes[i]
    if x.level != y.level ||
      x.tag != y.tag ||
      x.xref != y.xref ||
      x.parent != y.parent ||
      x.payload != y.payload {
      return false
    }
  }
  true
}