// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

// A document as it was written, not only as it reads.
//
// `Json` is what a configuration file means: a mapping is a map, a sequence is an
// array, and there is no room in either for the comment above a key, the flow
// style a short list was written in, or the tag that says `!!set`. That is the
// right type for a program that wants the value. It is the wrong type for a tool
// that has to write the file back, which is what this file is for: a tree that
// keeps the style, the anchor, the tag and the comments, and can be written back
// as the document it came from.

///|
/// How a scalar was written.
///
/// A block scalar carries the chomping indicator that was written with it, because
/// `|` and `|-` are different strings at the end of a line.
pub(all) enum ScalarStyle {
  Plain
  Single
  Double
  Literal(Int32)
  Folded(Int32)
} derive(Eq)
// `Eq` on a public enum is only useful when the methods are methods, and saying
// so here is what the compiler now asks for instead of promoting them silently.

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

///|
/// What a node is.
///
/// An alias is a node of its own rather than a copy of what it names: a document
/// with an anchor and three aliases is three references, and writing it back as
/// twelve copies would be a different document.
pub(all) enum Kind {
  Null
  Bool(Bool)
  Number(Double)
  Str(String)
  Sequence(Array[Node])
  Mapping(Array[(Node, Node)])
  Alias(String)
}

///|
/// One node of a document.
pub(all) struct Node {
  kind : Kind
  /// A scalar as it was written, before resolution: `014`, `yes`, `~`.
  ///
  /// Keeping the spelling is what makes a round trip a round trip: `014` read as
  /// twelve and written as `12` would be the same value in a different document.
  text : String
  /// How a scalar was written. Meaningless for a collection.
  style : ScalarStyle
  /// Whether a collection was written in flow style.
  flow : Bool
  anchor : String?
  tag : String?
  /// The comment lines written above this node, without their `#`.
  before : Array[String]
  /// The comment at the end of this node's last line, without its `#`.
  after : String?
}

///|
/// A plain scalar, which is how most of YAML is written.
pub fn Node::scalar(text : String) -> Node {
  {
    kind: Str(text),
    text,
    style: Plain,
    flow: false,
    anchor: None,
    tag: None,
    before: [],
    after: None,
  }
}

///|
/// A scalar of a known kind, keeping the spelling it was read from.
pub fn Node::of_kind(kind : Kind, text : String) -> Node {
  {
    kind,
    text,
    style: Plain,
    flow: false,
    anchor: None,
    tag: None,
    before: [],
    after: None,
  }
}

///|
/// A block sequence.
pub fn Node::seq(items : Array[Node]) -> Node {
  {
    kind: Sequence(items),
    text: "",
    style: Plain,
    flow: false,
    anchor: None,
    tag: None,
    before: [],
    after: None,
  }
}

///|
/// A block mapping.
pub fn Node::map(pairs : Array[(Node, Node)]) -> Node {
  {
    kind: Mapping(pairs),
    text: "",
    style: Plain,
    flow: false,
    anchor: None,
    tag: None,
    before: [],
    after: None,
  }
}

///|
/// This node, anchored.
pub fn Node::anchored(self : Node, name : String) -> Node {
  { ..self, anchor: Some(name), }
}

///|
/// This node, tagged.
pub fn Node::tagged(self : Node, tag : String) -> Node {
  { ..self, tag: Some(tag), }
}

///|
/// This node, written in flow style.
pub fn Node::in_flow(self : Node, flow : Bool) -> Node {
  { ..self, flow, }
}

///|
/// This node, with the comments written around it.
pub fn Node::commented(
  self : Node,
  before~ : Array[String],
  after~ : String,
) -> Node {
  { ..self, before, after: Some(after), }
}

///|
/// This node, written in a particular scalar style.
pub fn Node::styled(self : Node, style : ScalarStyle) -> Node {
  { ..self, style, }
}

///|
/// Whether two documents would be written the same way.
///
/// The comparison a round trip is: read, write, read again, and the second tree is
/// the first one. Positions are not in the tree, so this is equality of content.
pub fn Node::same(self : Node, other : Node) -> Bool {
  if self.text != other.text ||
    self.style != other.style ||
    self.flow != other.flow ||
    self.anchor != other.anchor ||
    self.tag != other.tag ||
    self.before != other.before ||
    self.after != other.after {
    return false
  }
  match (self.kind, other.kind) {
    (Null, Null) => true
    (Alias(a), Alias(b)) => a == b
    (Bool(a), Bool(b)) => a == b
    (Number(a), Number(b)) => a == b
    (Str(a), Str(b)) => a == b
    (Sequence(a), Sequence(b)) =>
      a.length() == b.length() &&
      ({
        let mut ok = true
        for i = 0; i < a.length(); i = i + 1 {
          if !a[i].same(b[i]) {
            ok = false
          }
        }
        ok
      })
    (Mapping(a), Mapping(b)) =>
      a.length() == b.length() &&
      ({
        let mut ok = true
        for i = 0; i < a.length(); i = i + 1 {
          if !a[i].0.same(b[i].0) || !a[i].1.same(b[i].1) {
            ok = false
          }
        }
        ok
      })
    _ => false
  }
}

// -- reading ------------------------------------------------------------------

///|
/// The comment text at the cursor, without its `#`, or the empty string.
fn Reader::dnote(self : Reader) -> String {
  if !self.at_note() {
    return ""
  }
  self.bump()
  let start = self.pos
  while !self.eof() && self.cur() != 0x0a && self.cur() != 0x0d {
    self.bump()
  }
  trim_unit(self.src[start:self.pos].to_owned())
}

///|
/// Skip the blank lines the cursor sits on, and only those: a comment is content
/// here.
///
/// The cursor ends on the first character that is not a blank — not at the start
/// of its line — because that is where the caller has to look to know how far the
/// line is indented (§8.2.1), and the indentation of the line before it says
/// nothing about it.
fn Reader::dblank(self : Reader) -> Unit {
  for ;; {
    self.gap()
    if !self.brk() {
      break
    }
  }
}

///|
/// The comment lines between here and the next node, in order.
///
/// A blank line between a comment and the node under it does not detach the
/// comment: it is still the comment above that node, which is where a writer would
/// put it back.
fn Reader::dcomments(self : Reader) -> Array[String] {
  let out : Array[String] = []
  for ;; {
    self.gap()
    if self.at_note() {
      out.push(self.dnote())
      let _ = self.brk()
      self.dblank()
      continue
    }
    break
  }
  out
}

///|
/// The kind a scalar has, given how it was written and what tag, if any, was put
/// in front of it.
fn Reader::dkind(
  self : Reader,
  text : String,
  quoted : Bool,
  tag : String?,
  at : At,
) -> Kind raise Refused {
  match self.resolve(text, quoted, tag, at) {
    Null => Null
    True => Bool(true)
    False => Bool(false)
    Number(n, ..) => Number(n)
    String(s) => Str(s)
    _ => Str(text)
  }
}

///|
/// An alias `*name`, kept as the reference it is.
fn Reader::dalias(self : Reader) -> String raise Refused {
  let at = self.at()
  if !self.schema.anchors {
    raise Disallowed(at, "an alias")
  }
  self.bump()
  let name = self.until_anchor()
  if name.length() == 0 {
    raise BadAnchor(at, "")
  }
  name
}

///|
/// A node, with the properties and comments written around it.
fn Reader::dnode(
  self : Reader,
  indent : Int32,
  depth : Int32,
  inline~ : Bool = false,
  dedent~ : Bool = false,
  before~ : Array[String] = [],
) -> Node raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  self.spend(self.at())
  let before = before + self.dcomments()
  self.gap()
  let start = (self.pos, self.line, self.bol)
  let opened = self.column()
  if self.cur() == 0x2a {
    let name = self.dalias()
    return {
      kind: Alias(name),
      text: "",
      style: Plain,
      flow: false,
      anchor: None,
      tag: None,
      before,
      after: None,
    }
  }
  let props = self.props(indent, dedent~)
  let moved = self.line != start.1
  let col = if moved { self.column() } else { opened }
  let node = self.dbody(
    indent,
    depth,
    props.tag,
    col,
    start,
    inline && !moved,
    moved,
    before,
  )
  match props.anchor {
    Some(name) => node.anchored(name)
    None => node
  }
}

///|
/// The content of a node, once its properties are off.
fn Reader::dbody(
  self : Reader,
  indent : Int32,
  depth : Int32,
  tag : String?,
  col : Int32,
  start : (Int32, Int32, Int32),
  inline : Bool,
  moved : Bool,
  before : Array[String],
) -> Node raise Refused {
  let at = self.at()
  let plain_of = fn(kind, text, style) -> Node {
    { kind, text, style, flow: false, anchor: None, tag, before, after: None, }
  }
  match self.cur() {
    0x5b | 0x7b => {
      if !self.schema.flow {
        raise Disallowed(at, "flow style")
      }
      let node = if self.cur() == 0x5b {
        self.dflow_seq(indent, depth)
      } else {
        self.dflow_map(indent, depth)
      }
      { ..node, before, tag, }
    }
    0x7c | 0x3e => {
      let folded = self.cur() == 0x3e
      let (text, chomp) = self.block_parts(indent, folded)
      let kind = self.dkind(text, true, tag, at)
      let style = if folded { Folded(chomp) } else { Literal(chomp) }
      plain_of(kind, text, style)
    }
    0x26 | 0x21 =>
      // A property that belongs to this node's content: a mapping whose first key
      // carries an anchor or a tag of its own.
      if self.key_line() {
        let node = self.dblock_map(col, depth)
        { ..node, before, tag, }
      } else {
        raise Unexpected(at, self.printable())
      }
    _ =>
      if self.at_item() {
        if inline {
          raise Unexpected(at, '-')
        }
        let node = self.dblock_seq(col, depth)
        { ..node, before, tag, }
      } else if self.at_explicit_key() {
        if inline {
          raise Unexpected(at, '?')
        }
        let node = self.dblock_map(col, depth)
        { ..node, before, tag, }
      } else {
        let own = (self.pos, self.line, self.bol)
        let own_col = self.column()
        let style = match self.cur() {
          0x27 => ScalarStyle::Single
          0x22 => ScalarStyle::Double
          _ => Plain
        }
        let (text, quoted) = self.scalar_text(Mode::Block, indent)
        let tail = (self.pos, self.line, self.bol)
        self.gap()
        if self.at_value_sep() {
          if inline {
            raise Unexpected(self.at(), ':')
          }
          let back = if moved { own } else { start }
          self.pos = back.0
          self.line = back.1
          self.bol = back.2
          let node = self.dblock_map(if moved { own_col } else { col }, depth)
          { ..node, before, tag, }
        } else {
          self.pos = tail.0
          self.line = tail.1
          self.bol = tail.2
          let after = self.dcomments()
          let kind = self.dkind(text, quoted, tag, at)
          {
            ..plain_of(kind, text, style),
            after: if after.length() > 0 {
              Some(after[0])
            } else {
              None
            },
          }
        }
      }
  }
}

///|
/// A block sequence, with its comments.
fn Reader::dblock_seq(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> Node raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let items : Array[Node] = []
  for ;; {
    let before = self.dcomments()
    self.dblank()
    if self.eof() || self.at_marker() || self.at_end_marker() {
      break
    }
    if self.column() != indent || !self.at_item() {
      if self.column() > indent && self.tabbed() {
        raise BadIndent(self.at())
      }
      break
    }
    if self.tabbed() {
      raise BadIndent(self.at())
    }
    self.bump()
    self.gap()
    if self.spent_line() {
      self.gap()
      self.note()
      let _ = self.brk()

      self.skip()
      if self.eof() || self.column() <= indent {
        items.push({ ..Node::of_kind(Null, ""), before, })
        continue
      }
      items.push(self.dnode(indent, depth + 1, before~))
    } else {
      items.push(self.dnode(indent, depth + 1, before~))
    }
  }
  Node::seq(items)
}

///|
/// A block mapping, with its comments and its explicit keys.
fn Reader::dblock_map(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> Node raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let pairs : Array[(Node, Node)] = []
  let merges : Array[(At, Node)] = []
  for ;; {
    let before = self.dcomments()
    self.dblank()
    if self.eof() || self.at_marker() || self.at_end_marker() {
      break
    }
    if self.column() != indent {
      if self.column() > indent && self.tabbed() {
        raise BadIndent(self.at())
      }
      break
    }
    if self.tabbed() {
      raise BadIndent(self.at())
    }
    let at = self.at()
    if self.at_explicit_key() {
      self.bump()
      self.gap()
      let key = if self.spent_line() {
        Node::of_kind(Null, "")
      } else {
        self.dnode(indent, depth + 1, before~)
      }
      self.skip()
      let value = if self.column() == indent && self.at_value_sep() {
        self.bump()
        self.gap()
        if self.spent_line() {
          self.dpair_value(indent, depth)
        } else {
          self.dnode(indent, depth + 1, dedent=true)
        }
      } else {
        Node::of_kind(Null, "")
      }
      pairs.push((key, value))
      continue
    }
    if self.at_item() {
      break
    }
    let kp = self.props(indent)
    self.gap()
    let key = if self.cur() == 0x2a {
      if kp.anchor is Some(_) || kp.tag is Some(_) {
        raise Unexpected(at, '*')
      }
      let name = self.dalias()
      { ..Node::of_kind(Alias(name), ""), before, }
    } else {
      let style = match self.cur() {
        0x27 => ScalarStyle::Single
        0x22 => ScalarStyle::Double
        _ => Plain
      }
      let (text, quoted) = self.scalar_text(Mode::Key, indent)
      if self.line != at.line {
        raise Unexpected(self.at(), '\n')
      }
      self.gap()
      guard self.at_value_sep() else {
        if text.length() == 0 && !quoted && kp.tag is None && kp.anchor is None {
          break
        }
        raise Unexpected(at, self.printable())
      }
      let kind = self.dkind(text, quoted, kp.tag, at)
      {
        kind,
        text,
        style,
        flow: false,
        anchor: kp.anchor,
        tag: kp.tag,
        before,
        after: None,
      }
    }
    self.bump()
    // A bare `<<` is the merge key in the versions that have one.
    if self.schema.merge &&
      !(key.style is Single || key.style is Double) &&
      key.text == "<<" {
      merges.push((at, self.dpair_value(indent, depth)))
      continue
    }
    let value = self.dpair_value(indent, depth)
    pairs.push((key, value))
  }
  if merges.length() > 0 {
    for pair in merged_pairs(merges) {
      pairs.push(pair)
    }
  }
  Node::map(pairs)
}

///|
/// The pairs a merge key brought in, at the place the key was.
fn merged_pairs(merges : Array[(At, Node)]) -> Array[(Node, Node)] {
  let out : Array[(Node, Node)] = []
  for (_, source) in merges {
    match source.kind {
      Mapping(pairs) =>
        for pair in pairs {
          out.push(pair)
        }
      Sequence(items) =>
        for item in items {
          match item.kind {
            Mapping(pairs) =>
              for pair in pairs {
                out.push(pair)
              }
            _ => ()
          }
        }
      _ => ()
    }
  }
  out
}

///|
/// The value of a pair whose `:` is behind the cursor.
fn Reader::dpair_value(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> Node raise Refused {
  self.gap()
  if self.spent_line() {
    self.gap()
    self.note()
    let _ = self.brk()

    self.skip()
    if self.eof() || self.at_marker() || self.at_end_marker() {
      Node::of_kind(Null, "")
    } else if self.column() > indent {
      self.dnode(indent, depth + 1, dedent=true)
    } else if self.column() == indent && self.at_item() {
      self.dblock_seq(indent, depth + 1)
    } else {
      Node::of_kind(Null, "")
    }
  } else {
    self.dnode(indent, depth + 1, inline=true, dedent=true)
  }
}

///|
/// A flow sequence.
fn Reader::dflow_seq(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> Node raise Refused {
  let at = self.at()
  self.bump()
  let items : Array[Node] = []
  for ;; {
    self.flow_gap(indent, 0x5d)
    if self.eof() {
      raise Truncated(at)
    }
    self.no_marker(at)
    if self.cur() == 0x5d {
      self.bump()
      break
    }
    if self.cur() == 0x2c {
      raise Unexpected(self.at(), ',')
    }
    items.push(self.dflow_node(indent, depth + 1))
    self.flow_gap(indent, 0x5d)
    if self.cur() == 0x2c {
      self.bump()
    } else if self.cur() != 0x5d {
      raise Unexpected(self.at(), self.printable())
    }
  }
  { ..Node::seq(items), flow: true, }
}

///|
/// A flow mapping.
fn Reader::dflow_map(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> Node raise Refused {
  let at = self.at()
  self.bump()
  let pairs : Array[(Node, Node)] = []
  for ;; {
    self.flow_gap(indent, 0x7d)
    if self.eof() {
      raise Truncated(at)
    }
    self.no_marker(at)
    if self.cur() == 0x7d {
      self.bump()
      break
    }
    if self.cur() == 0x2c {
      raise Unexpected(self.at(), ',')
    }
    let kat = self.at()
    if self.at_explicit_key() {
      self.bump()
      self.gap()
    }
    let key = self.dflow_node(indent, depth + 1)
    self.flow_gap(indent, 0x7d)
    let value = if self.cur() == 0x3a {
      self.bump()
      self.flow_gap(indent, 0x7d)
      if self.cur() is (0x2c | 0x7d) {
        Node::of_kind(Null, "")
      } else {
        self.dflow_node(indent, depth + 1)
      }
    } else {
      Node::of_kind(Null, "")
    }
    let _ = kat
    pairs.push((key, value))
    self.flow_gap(indent, 0x7d)
    if self.cur() == 0x2c {
      self.bump()
    } else if self.cur() != 0x7d {
      raise Unexpected(self.at(), self.printable())
    }
  }
  { ..Node::map(pairs), flow: true, }
}

///|
/// One node inside a flow collection.
fn Reader::dflow_node(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> Node raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  self.spend(self.at())
  self.flow_gap(indent, 0x00)
  if self.cur() == 0x2a {
    let name = self.dalias()
    return Node::of_kind(Alias(name), "")
  }
  let props = self.props(-1)
  self.flow_gap(indent, 0x00)
  let at = self.at()
  let node = match self.cur() {
    0x5b =>
      if !self.schema.flow {
        raise Disallowed(at, "flow style")
      } else {
        self.dflow_seq(indent, depth)
      }
    0x7b =>
      if !self.schema.flow {
        raise Disallowed(at, "flow style")
      } else {
        self.dflow_map(indent, depth)
      }
    _ => {
      let style = match self.cur() {
        0x27 => ScalarStyle::Single
        0x22 => ScalarStyle::Double
        _ => Plain
      }
      let (text, quoted) = self.scalar_text(Mode::Flow, -1)
      let kind = self.dkind(text, quoted, props.tag, at)
      {
        kind,
        text,
        style,
        flow: false,
        anchor: None,
        tag: props.tag,
        before: [],
        after: None,
      }
    }
  }
  match props.anchor {
    Some(name) => node.anchored(name)
    None => node
  }
}

///|
/// One document, as a tree.
pub fn parse(
  src : StringView,
  flavor~ : Flavor = strict,
  schema~ : Schema = core,
) -> Node raise Refused {
  let docs = parse_all(src, flavor~, schema~)
  if docs.length() == 0 {
    Node::of_kind(Null, "")
  } else {
    docs[0]
  }
}

///|
/// Every document in the stream, as trees.
///
/// A stream with nothing in it holds no document, which is what `loads_all` says
/// too; `parse` is where a single document is owed, and it gives the null one.
pub fn parse_all(
  src : StringView,
  flavor~ : Flavor = strict,
  schema~ : Schema = core,
) -> Array[Node] raise Refused {
  let reader = Reader::new(src, flavor, schema)
  let out : Array[Node] = []
  let mut last = -1
  for ;; {
    if reader.eof() {
      break
    }
    // Every turn of this loop has to consume something, or a document that reads
    // as nothing would spin here for ever.
    if reader.pos == last {
      raise Unexpected(reader.at(), reader.printable())
    }
    last = reader.pos
    // The frame of a stream — directives, `---`, `...`, comments and blank lines —
    // is not its content: a tree keeps what a document says, and the writer puts
    // the frame back from it. They may come in any order (§9.1.1), so they are
    // read in one loop, and what a `---` opened is decided where it ends.
    let mut marked = false
    let before : Array[String] = []
    let mut open = true
    while open {
      if reader.cur() == 0x25 {
        reader.directive()
        continue
      }
      if reader.at_marker() {
        // A second `---` before any content closes an empty document.
        if marked {
          out.push({ ..Node::of_kind(Null, ""), before, })
        }
        let _ = reader.marker("---")
        marked = true
        reader.gap()
        if !(reader.eof() || reader.at_note() || reader.at_break()) {
          // A node on the marker's own line: `--- "foo"` is a document.
          open = false
        }
        continue
      }
      if reader.at_end_marker() {
        let _ = reader.marker("...")
        reader.gap()
        if reader.at_note() {
          let _ = reader.dnote()
        }
        let _ = reader.brk()
        // A `---` that closed before it held anything held an empty document.
        if marked {
          out.push({ ..Node::of_kind(Null, ""), before, })
          marked = false
        }
        continue
      }
      let notes = reader.dcomments()
      let at = reader.pos
      for note in notes {
        before.push(note)
      }
      reader.dblank()
      if reader.pos != at || notes.length() > 0 {
        continue
      }
      open = false
    }
    if reader.eof() {
      // A stream that ends on its frame holds whatever the frame opened: a `---`
      // holds an empty document, and bare comments hold nothing at all.
      if marked {
        out.push({ ..Node::of_kind(Null, ""), before, })
      } else if before.length() > 0 {
        out.push({ ..Node::of_kind(Null, ""), before, })
      }
      break
    }
    out.push(reader.dnode(-1, 0, before~))
    reader.skip()
    if reader.at_end_marker() {
      let _ = reader.marker("...")
      reader.gap()
      if reader.at_note() {
        let _ = reader.dnote()
      }
      let _ = reader.brk()
    }
  }
  out
}

// -- writing ------------------------------------------------------------------

///|
/// A single-quoted spelling of `text`.
fn dquote_single(text : String) -> String {
  let out = StringBuilder()
  out.write_char('\'')
  for i = 0; i < text.length(); i = i + 1 {
    if text[i].to_int() == 0x27 {
      out.write_char('\'')
    }
    out.write_char(text[i].unsafe_to_char())
  }
  out.write_char('\'')
  out.to_string()
}

///|
/// A double-quoted spelling of `text`, with the escapes a document needs.
fn dquote_double(text : String) -> String {
  let out = StringBuilder()
  out.write_char('"')
  for i = 0; i < text.length(); i = i + 1 {
    let c = text[i].to_int()
    if c == 0x22 {
      out.write_string("\\\"")
    } else if c == 0x5c {
      out.write_string("\\\\")
    } else if c == 0x0a {
      out.write_string("\\n")
    } else if c == 0x09 {
      out.write_string("\\t")
    } else if c == 0x0d {
      out.write_string("\\r")
    } else if c < 0x20 {
      out.write_string("\\x")
      out.write_string(hex_of(c))
    } else {
      out.write_char(text[i].unsafe_to_char())
    }
  }
  out.write_char('"')
  out.to_string()
}

///|
/// Two hexadecimal digits, lower case.
fn hex_of(c : Int32) -> String {
  let digits = "0123456789abcdef"
  let high = digits[c / 16 % 16].unsafe_to_char()
  let low = digits[c % 16].unsafe_to_char()
  "\{high}\{low}"
}

///|
/// A block scalar's body: one line of content per line, indented by `indent`.
///
/// The text is what a reader gave back, so the line breaks it holds are content.
/// A literal scalar keeps them by putting each line on its own line; a folded
/// scalar's break between two ordinary lines would read back as a space, so a
/// break the content really holds is written as an empty line (§6.5). An empty
/// line carries no indentation, so a body never ends in white space.
///
/// The body always ends with the break that closes its last content line, which
/// is also the break that starts whatever is written next — `-` is what takes
/// that break off again on the way back in, so the writer never has to skip it.
fn block_body(text : String, indent : String, folded : Bool) -> String {
  let out = StringBuilder()
  let lines = text.split("\n").to_array()
  // The break after the final content line is not part of the text.
  let mut last = lines.length() - 1
  if lines[last].length() == 0 {
    last = last - 1
  }
  let mut pending = 0
  let mut started = false
  let mut more_before = false
  for i = 0; i <= last; i = i + 1 {
    let line = lines[i]
    if line.length() == 0 {
      pending = pending + 1
      continue
    }
    let more = is_blank(line[0].to_int())
    let blanks = if folded && started && !more_before && !more {
      pending + 1
    } else {
      pending
    }
    for k = 0; k < blanks; k = k + 1 {
      out.write_char('\n')
    }
    out.write_string(indent)
    out.write_string(line.to_owned())
    out.write_char('\n')
    pending = 0
    started = true
    more_before = more
  }
  // Whatever is still pending is the trailing break a `+` keeps and a `-` strips.
  for k = 0; k < pending; k = k + 1 {
    out.write_char('\n')
  }
  out.to_string()
}

///|
/// A tag as it is written: a tag in the standard prefix keeps the `!!` shorthand
/// a reader resolves it to, a local tag is already written in full, and anything
/// else has to be spelled out between `!<` and `>`.
fn tag_text(tag : String) -> String {
  let standard = "tag:yaml.org,2002:"
  if tag.has_prefix(standard) {
    "!!" + tag[standard.length():].to_owned()
  } else if tag.has_prefix("!") {
    tag
  } else {
    "!<" + tag + ">"
  }
}

///|
/// The properties a node carries: its tag and its anchor, in that order, with a
/// single space between them and nothing at either end.
fn props_text(node : Node) -> String {
  let parts : Array[String] = []
  match node.tag {
    Some(tag) => parts.push(tag_text(tag))
    None => ()
  }
  match node.anchor {
    Some(name) => parts.push("&" + name)
    None => ()
  }
  parts.join(" ")
}

///|
/// The properties followed by the space that separates them from what they
/// describe: nothing at all when the node carries none.
fn props_prefix(node : Node) -> String {
  let text = props_text(node)
  if text.length() == 0 {
    text
  } else {
    text + " "
  }
}

///|
/// The properties written after something that already ended: nothing, or a
/// space and then the properties, so a line never ends in white space.
fn props_after(out : StringBuilder, node : Node) -> Unit {
  let text = props_text(node)
  if text.length() > 0 {
    out.write_char(' ')
    out.write_string(text)
  }
}

///|
/// A scalar on one line, in the style it was written.
fn scalar_text_of(node : Node) -> String {
  match node.style {
    Single => dquote_single(node.text)
    Double => dquote_double(node.text)
    _ =>
      if node.text.length() == 0 {
        // An empty plain scalar is written as nothing at all, which is how it read.
        ""
      } else {
        node.text
      }
  }
}

///|
/// A node that fits on one line, without its properties: a scalar, an alias, or a
/// flow collection of those. `None` means the node needs lines of its own.
fn inline_text(node : Node) -> String? {
  match node.kind {
    Sequence(items) if node.flow => {
      let parts : Array[String] = []
      for item in items {
        guard inline_text(item) is Some(text) else { return None }
        parts.push(props_prefix(item) + text)
      }
      Some("[" + parts.join(", ") + "]")
    }
    Mapping(pairs) if node.flow => {
      let parts : Array[String] = []
      for pair in pairs {
        guard inline_text(pair.0) is Some(k) else { return None }
        guard inline_text(pair.1) is Some(v) else { return None }
        parts.push(props_prefix(pair.0) + k + ": " + props_prefix(pair.1) + v)
      }
      Some("{" + parts.join(", ") + "}")
    }
    Alias(name) => Some("*" + name)
    Sequence(_) | Mapping(_) => None
    _ =>
      match node.style {
        Literal(_) | Folded(_) => None
        _ => Some(scalar_text_of(node))
      }
  }
}

///|
/// A block scalar written where the cursor is: the indicator, then the body.
///
/// The body's own indentation is part of the scalar, so `body_at` is the column
/// every line of the content starts in, and `text` keeps whatever further
/// indentation the content itself had.
fn write_block_scalar(
  out : StringBuilder,
  node : Node,
  body_at : Int32,
) -> Unit {
  let (chomp, folded) = match node.style {
    Literal(n) => (n, false)
    Folded(n) => (n, true)
    _ => (0, false)
  }
  out.write_char(if folded { '>' } else { '|' })
  out.write_string(chomp_mark(chomp))
  out.write_char('\n')
  out.write_string(block_body(node.text, spaces(body_at), folded))
}

///|
/// Emit a document as YAML, keeping the styles and comments it was read with.
///
/// `indent` is how far a nested block is indented, and `from` is how far the root
/// already is — a document embedded in another one needs the second.
pub fn emit(node : Node, indent~ : Int32 = 2, from~ : Int32 = 0) -> String {
  let out = StringBuilder()
  for comment in node.before {
    write_comment(out, comment, from)
  }
  match node.kind {
    Sequence(_) | Mapping(_) =>
      if node.flow {
        out.write_string(spaces(from))
        out.write_string(props_prefix(node))
        out.write_string(inline_text(node).unwrap_or("[]"))
        write_note(out, node)
        out.write_char('\n')
      } else {
        // A block collection's properties take a line of their own, so they
        // describe the collection rather than whatever it holds (§8.2.2).
        if props_text(node).length() > 0 {
          out.write_string(spaces(from))
          out.write_string(props_text(node))
          out.write_char('\n')
        }
        write_block(out, node, indent, from)
      }
    _ => {
      out.write_string(spaces(from))
      out.write_string(props_prefix(node))
      let mut block = false
      match node.style {
        Literal(_) | Folded(_) => {
          block = true
          write_block_scalar(out, node, from + indent)
        }
        _ => out.write_string(scalar_text_of(node))
      }
      write_note(out, node)
      if !block {
        out.write_char('\n')
      }
    }
  }
  out.to_string()
}

///|
/// The chomping indicator a block scalar was written with.
fn chomp_mark(chomp : Int32) -> String {
  if chomp < 0 {
    "-"
  } else if chomp > 0 {
    "+"
  } else {
    ""
  }
}

///|
/// `n` spaces.
fn spaces(n : Int32) -> String {
  let out = StringBuilder()
  for i = 0; i < n; i = i + 1 {
    out.write_char(' ')
  }
  out.to_string()
}

///|
/// A block collection, one entry per line, with its entries starting in column
/// `at`: the root sits at zero, and a nested block is one indent further in than
/// the key that holds it.
fn write_block(
  out : StringBuilder,
  node : Node,
  indent : Int32,
  at : Int32,
  on_dash~ : Bool = false,
) -> Unit {
  let inner = at
  let mut first = true
  match node.kind {
    Sequence(items) => {
      if items.length() == 0 {
        out.write_string(spaces(at))
        out.write_string("[]")
        out.write_char('\n')
        return
      }
      for item in items {
        for comment in item.before {
          write_comment(out, comment, inner)
        }
        if first && on_dash {
          // The `-` that opened this entry is the first line's indentation.
        } else {
          out.write_string(spaces(inner))
        }
        first = false
        out.write_char('-')
        match inline_text(item) {
          Some(text) =>
            if text.length() == 0 && props_text(item).length() == 0 {
              out.write_char('\n')
            } else {
              out.write_char(' ')
              out.write_string(props_prefix(item))
              out.write_string(text)
              write_note(out, item)
              out.write_char('\n')
            }
          None =>
            match item.style {
              Literal(_) | Folded(_) => {
                // The indicator shares the `-` line and the body starts under it.
                out.write_char(' ')
                out.write_string(props_prefix(item))
                write_block_scalar(out, item, inner + indent)
              }
              _ =>
                match item.kind {
                  Sequence(_) =>
                    if props_text(item).length() == 0 {
                      // A sequence in a sequence shares the `-` line: `- - one`.
                      out.write_char(' ')
                      write_block(
                        out,
                        item,
                        indent,
                        inner + indent,
                        on_dash=true,
                      )
                    } else {
                      // Its properties take the `-` line, so the entries movein.
                      out.write_char(' ')
                      out.write_string(props_text(item))
                      out.write_char('\n')
                      write_block(out, item, indent, inner + indent)
                    }
                  Mapping(_) => {
                    // A block mapping under a `-` starts on the `-` line, which is
                    // what makes `- key: value` a list of mappings.
                    out.write_char(' ')
                    out.write_string(props_prefix(item))
                    write_block(out, item, indent, inner + indent, on_dash=true)
                  }
                  _ => {
                    out.write_char('\n')
                    block_or_scalar(out, item, indent, inner + indent)
                  }
                }
            }
        }
      }
    }
    Mapping(pairs) => {
      if pairs.length() == 0 {
        out.write_string(spaces(at))
        out.write_string("{}")
        out.write_char('\n')
        return
      }
      for pair in pairs {
        let key = pair.0
        let value = pair.1
        for comment in key.before {
          write_comment(out, comment, inner)
        }
        if first && on_dash {
          // The `-` that opened this entry is the first line's indentation.
        } else {
          out.write_string(spaces(inner))
        }
        first = false
        // A key that is a collection is written explicitly, which is the one form
        // that never leaves the reader guessing where the key ends (§8.2.2).
        let key_form = match key.kind {
          Sequence(_) | Mapping(_) => None
          _ => inline_text(key)
        }
        match key_form {
          Some(text) => {
            out.write_string(props_prefix(key))
            out.write_string(text)
            out.write_char(':')
          }
          None => {
            out.write_char('?')
            write_key(out, key, indent, inner + indent)
            out.write_string(spaces(inner))
            out.write_char(':')
          }
        }
        match inline_text(value) {
          Some(text) =>
            if text.length() == 0 && props_text(value).length() == 0 {
              write_note(out, key)
              out.write_char('\n')
            } else {
              out.write_char(' ')
              out.write_string(props_prefix(value))
              out.write_string(text)
              write_note(out, value)
              out.write_char('\n')
            }
          None =>
            match value.style {
              Literal(_) | Folded(_) => {
                // The indicator belongs on the key's line, and the body under it.
                out.write_char(' ')
                out.write_string(props_prefix(value))
                write_block_scalar(out, value, inner + indent)
              }
              _ => {
                // A block collection's properties belong on the key's line too, so
                // the anchor is the map's rather than its first key's (§8.2.1).
                props_after(out, value)
                write_note(out, key)
                out.write_char('\n')
                block_or_scalar(out, value, indent, inner + indent)
              }
            }
        }
      }
    }
    _ => ()
  }
}

///|
/// A node on lines of its own starting at column `at`, for a node whose
/// properties the caller has already written: a block collection, or a scalar
/// that cannot share a line with what introduced it.
fn block_or_scalar(
  out : StringBuilder,
  node : Node,
  indent : Int32,
  at : Int32,
) -> Unit {
  match node.kind {
    Sequence(_) | Mapping(_) =>
      if node.flow {
        out.write_string(spaces(at))
        out.write_string(inline_text(node).unwrap_or("[]"))
        write_note(out, node)
        out.write_char('\n')
      } else {
        write_block(out, node, indent, at)
      }
    _ => {
      out.write_string(spaces(at))
      let mut block = false
      match node.style {
        Literal(_) | Folded(_) => {
          block = true
          write_block_scalar(out, node, at + indent)
        }
        _ => out.write_string(scalar_text_of(node))
      }
      write_note(out, node)
      if !block {
        out.write_char('\n')
      }
    }
  }
}

///|
/// The key of an explicit pair: on the `?` line when it fits there, and on the
/// lines under it when it does not.
fn write_key(
  out : StringBuilder,
  key : Node,
  indent : Int32,
  at : Int32,
) -> Unit {
  match inline_text(key) {
    Some(text) => {
      out.write_char(' ')
      out.write_string(props_prefix(key))
      out.write_string(text)
      write_note(out, key)
      out.write_char('\n')
    }
    None => {
      props_after(out, key)
      out.write_char('\n')
      block_or_scalar(out, key, indent, at)
    }
  }
}

///|
/// A comment line at column `at`.
fn write_comment(out : StringBuilder, text : String, at : Int32) -> Unit {
  out.write_string(spaces(at))
  out.write_char('#')
  if text.length() > 0 {
    out.write_char(' ')
    out.write_string(text)
  }
  out.write_char('\n')
}

///|
/// The comment written beside `node`, if it had one.
fn write_note(out : StringBuilder, node : Node) -> Unit {
  match node.after {
    Some(note) => {
      out.write_string(" # ")
      out.write_string(note)
    }
    None => ()
  }
}

// -- the value a tree stands for -------------------------------------------------

///|
/// A key as the string a `Json` object keys it by.
///
/// `Json` has only string keys, so a mapping written with a collection as its key
/// keeps that key as the JSON text of the collection: not the same document, but
/// not a silent collision either.
fn key_string(value : Json) -> String {
  match value {
    String(s) => s
    Number(n, ..) => n.to_string()
    True => "true"
    False => "false"
    Null => "null"
    _ => value.stringify()
  }
}

///|
/// The `Json` this document denotes, with its aliases expanded.
pub fn Node::to_json(self : Node) -> Json {
  self.to_json_with(Map([]))
}

///|
/// The `Json` this node denotes, resolving aliases through `anchors`.
fn Node::to_json_with(self : Node, anchors : Map[String, Json]) -> Json {
  let value = match self.kind {
    Null => Json::null()
    Bool(b) => Json::boolean(b)
    Number(n) => Json::number(n)
    Str(s) => Json::string(s)
    Alias(name) =>
      match anchors.get(name) {
        Some(v) => v
        None => Json::null()
      }
    Sequence(items) => {
      let out : Array[Json] = []
      for item in items {
        out.push(item.to_json_with(anchors))
      }
      Json::array(out)
    }
    Mapping(pairs) => {
      let out : Map[String, Json] = Map([])
      for pair in pairs {
        let k = key_string(pair.0.to_json_with(anchors))
        out[k] = pair.1.to_json_with(anchors)
      }
      Json::object(out)
    }
  }
  match self.anchor {
    Some(name) => {
      anchors[name] = value
      value
    }
    None => value
  }
}

///|
/// A tree for a `Json` value: the document a program would write for it.
pub fn Node::of_json(value : Json) -> Node {
  match value {
    Null => Node::of_kind(Null, "")
    True => Node::of_kind(Bool(true), "true")
    False => Node::of_kind(Bool(false), "false")
    Number(n, ..) => Node::of_kind(Number(n), n.to_string())
    String(s) => Node::of_kind(Str(s), s)
    Array(items) => Node::seq(items.map(fn(item) { Node::of_json(item) }))
    Object(fields) =>
      Node::map(
        fields
        .to_array()
        .map(fn(pair) {
          (Node::of_kind(Str(pair.0), pair.0), Node::of_json(pair.1))
        }),
      )
  }
}