///|
/// A minimal streaming XML writer.
pub struct Writer {
  priv buf : StringBuilder
} derive(Debug)

///|
pub fn Writer::new() -> Writer {
  { buf: StringBuilder() }
}

///|
/// Return the accumulated document.
pub fn Writer::to_string(self : Writer) -> String {
  self.buf.to_string()
}

///|
/// Write the XML declaration used by RSS/Atom output.
pub fn Writer::write_decl(self : Writer) -> Unit {
  self.buf.write_string("")
}

///|
/// Write an opening tag ``.
pub fn Writer::open(self : Writer, name : String) -> Unit {
  self.buf.write_string("<\{name}>")
}

///|
/// Write an opening tag with attributes, ``.
/// Attribute values are escaped.
pub fn Writer::open_with(
  self : Writer,
  name : String,
  attrs~ : Array[(String, String)],
) -> Unit {
  self.buf.write_string("<\{name}")
  for pair in attrs {
    let (k, v) = pair
    self.buf.write_string(" \{k}=\"\{escape_attr(v)}\"")
  }
  self.buf.write_string(">")
}

///|
/// Write a closing tag ``.
pub fn Writer::close(self : Writer, name : String) -> Unit {
  self.buf.write_string("")
}

///|
/// Write a self-closing tag; attributes are escaped.
pub fn Writer::empty(
  self : Writer,
  name : String,
  attrs? : Array[(String, String)] = [],
) -> Unit {
  if attrs.length() == 0 {
    self.buf.write_string("<\{name}/>")
  } else {
    self.buf.write_string("<\{name}")
    for pair in attrs {
      let (k, v) = pair
      self.buf.write_string(" \{k}=\"\{escape_attr(v)}\"")
    }
    self.buf.write_string("/>")
  }
}

///|
/// Write escaped character data.
pub fn Writer::text(self : Writer, content : String) -> Unit {
  self.buf.write_string(escape_text(content))
}

///|
/// Write pre-escaped / raw markup. The caller guarantees well-formedness.
pub fn Writer::raw(self : Writer, content : String) -> Unit {
  self.buf.write_string(content)
}

///|
/// Write a CDATA section, splitting any `]]>` occurrences so the
/// section stays well-formed.
pub fn Writer::cdata(self : Writer, content : String) -> Unit {
  self.buf.write_string("", new="]]]]>"))
  self.buf.write_string("]]>")
}

///|
/// Escape text-node content.
pub fn escape_text(s : String) -> String {
  if !(s.contains("&") ||
    s.contains("<") ||
    s.contains(">") ||
    s.contains("\"") ||
    s.contains("'")) {
    return s
  }
  let sb = StringBuilder()
  for c in s {
    match c {
      '&' => sb.write_string("&")
      '<' => sb.write_string("<")
      '>' => sb.write_string(">")
      '"' => sb.write_string(""")
      '\'' => sb.write_string("'")
      _ => sb.write_char(c)
    }
  }
  sb.to_string()
}

///|
/// Escape attribute-value content.
pub fn escape_attr(s : String) -> String {
  if !(s.contains("&") ||
    s.contains("<") ||
    s.contains(">") ||
    s.contains("\"") ||
    s.contains("'")) {
    return s
  }
  let sb = StringBuilder()
  for c in s {
    match c {
      '&' => sb.write_string("&")
      '<' => sb.write_string("<")
      '>' => sb.write_string(">")
      '"' => sb.write_string(""")
      '\'' => sb.write_string("'")
      _ => sb.write_char(c)
    }
  }
  sb.to_string()
}