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

///|
/// Where the reader is, and what the stream has told it so far.
///
/// The cursor is a character offset rather than a line index because YAML's block
/// scalars, multi-line flow collections and folded plain scalars all cross line
/// boundaries: a reader that splits into lines first cannot express them.
priv struct Reader {
  src : StringView
  mut pos : Int
  mut line : Int
  // Offset of the first character of the current line, so a column is a subtraction.
  mut bol : Int
  flavor : Flavor
  anchors : Map[String, Json]
  // `%TAG` handle to prefix, per document (YAML 1.2.2 §6.8.2).
  handles : Map[String, String]
  // Nodes produced so far, against the alias budget.
  mut spent : Int
}

///|
fn Reader::new(src : StringView, flavor : Flavor) -> Reader {
  {
    src,
    pos: 0,
    line: 1,
    bol: 0,
    flavor,
    anchors: Map([]),
    handles: Map([]),
    spent: 0,
  }
}

// -- the cursor ---------------------------------------------------------------

///|
fn Reader::at(self : Reader) -> At {
  { at: self.pos, line: self.line, column: self.pos - self.bol + 1, }
}

///|
fn Reader::eof(self : Reader) -> Bool {
  self.pos >= self.src.length()
}

///|
/// The code unit at the cursor, or `-1` at end of stream.
fn Reader::cur(self : Reader) -> Int {
  if self.pos < self.src.length() {
    self.src[self.pos].to_int()
  } else {
    -1
  }
}

///|
/// The code unit `k` past the cursor, or `-1`.
fn Reader::ahead(self : Reader, k : Int) -> Int {
  if self.pos + k < self.src.length() {
    self.src[self.pos + k].to_int()
  } else {
    -1
  }
}

///|
/// The column the cursor is in, counting from zero, which is what indentation is
/// compared against.
fn Reader::column(self : Reader) -> Int {
  self.pos - self.bol
}

///|
/// Advance one character, keeping the line count right.
fn Reader::bump(self : Reader) -> Unit {
  if self.pos >= self.src.length() {
    return
  }
  let c = self.src[self.pos].to_int()
  self.pos = self.pos + 1
  if c == 0x0a {
    self.line = self.line + 1
    self.bol = self.pos
  } else if c == 0x0d {
    // A lone CR ends a line too (§5.4); CRLF counts once.
    if self.cur() != 0x0a {
      self.line = self.line + 1
      self.bol = self.pos
    }
  }
}

///|
/// Consume one line break, if the cursor is on one.
fn Reader::brk(self : Reader) -> Bool {
  match self.cur() {
    0x0d => {
      self.bump()
      if self.cur() == 0x0a {
        self.bump()
      }
      true
    }
    0x0a => {
      self.bump()
      true
    }
    _ => false
  }
}

///|
/// Skip spaces and tabs, which separate tokens but never carry structure — only a
/// space counts as indentation (§5.5), and a tab where indentation is expected is
/// caught by the caller comparing columns.
fn Reader::gap(self : Reader) -> Unit {
  while self.cur() == 0x20 || self.cur() == 0x09 {
    self.bump()
  }
}

///|
/// Skip a comment, if one starts here, up to but not including the line break.
fn Reader::note(self : Reader) -> Unit {
  if self.cur() == 0x23 {
    while !self.eof() && self.cur() != 0x0a && self.cur() != 0x0d {
      self.bump()
    }
  }
}

///|
/// Skip to the first character of the next line that carries content, passing over
/// blank lines and comment-only lines. Leaves the cursor on that line's
/// indentation, so the caller reads a column.
fn Reader::skip(self : Reader) -> Unit {
  for ;; {
    self.gap()
    self.note()
    if !self.brk() {
      return
    }
  }
}

///|
/// Whether the rest of this line is blank or a comment.
fn Reader::spent_line(self : Reader) -> Bool {
  let save = (self.pos, self.line, self.bol)
  self.gap()
  let done = self.eof() ||
    self.cur() == 0x23 ||
    self.cur() == 0x0a ||
    self.cur() == 0x0d
  self.pos = save.0
  self.line = save.1
  self.bol = save.2
  done
}

// -- the stream ---------------------------------------------------------------

///|
/// Every document in the stream (§9.1), in order.
fn Reader::stream(self : Reader) -> Array[Json] raise Refused {
  let out : Array[Json] = []
  for ;; {
    self.skip()
    if self.eof() {
      break
    }
    out.push(self.document())
  }
  out
}

///|
/// One document: its directives, an optional `---`, its root node, and an optional
/// `...` (§9.1.2, §9.1.3).
fn Reader::document(self : Reader) -> Json raise Refused {
  self.handles.clear()
  self.anchors.clear()
  let mut marked = false
  for ;; {
    self.skip()
    if self.cur() == 0x25 {
      self.directive()
      continue
    }
    if self.marker("---") {
      marked = true
      self.gap()
      self.note()
      let _ = self.brk()
    }
    break
  }
  self.skip()
  // A document that is only a marker, or only directives, holds nothing.
  let root = if self.eof() ||
    self.at_end_marker() ||
    (marked && self.at_marker()) {
    Json::null()
  } else {
    self.node(-1, 0)
  }
  self.skip()
  if self.marker("...") {
    self.gap()
    self.note()
    let _ = self.brk()
  }
  root
}

///|
/// Whether a document marker — `---` or `...` — starts at the cursor, which is only
/// a marker at the start of a line and followed by a space or a line break.
fn Reader::marker(self : Reader, want : String) -> Bool {
  if self.column() != 0 {
    return false
  }
  for i = 0; i < 3; i = i + 1 {
    if self.ahead(i) != want[i].to_int() {
      return false
    }
  }
  let after = self.ahead(3)
  if after != -1 &&
    after != 0x20 &&
    after != 0x09 &&
    after != 0x0a &&
    after != 0x0d {
    return false
  }
  for i = 0; i < 3; i = i + 1 {
    self.bump()
  }
  true
}

///|
/// Whether a `---` starts here, without consuming it.
fn Reader::at_marker(self : Reader) -> Bool {
  self.column() == 0 &&
  self.ahead(0) == 0x2d &&
  self.ahead(1) == 0x2d &&
  self.ahead(2) == 0x2d &&
  self.ahead(3) is (-1 | 0x20 | 0x09 | 0x0a | 0x0d)
}

///|
/// Whether a `...` starts here, without consuming it.
fn Reader::at_end_marker(self : Reader) -> Bool {
  self.column() == 0 &&
  self.ahead(0) == 0x2e &&
  self.ahead(1) == 0x2e &&
  self.ahead(2) == 0x2e &&
  self.ahead(3) is (-1 | 0x20 | 0x09 | 0x0a | 0x0d)
}

///|
/// A `%YAML` or `%TAG` directive (§6.8).
///
/// `%YAML` is read and checked for a major version this reader speaks; a minor
/// version it does not know is allowed, as §6.8.1 requires. Any other directive is
/// refused rather than ignored: a document that asks for something unimplemented
/// should say so, not load as if it had not asked.
fn Reader::directive(self : Reader) -> Unit raise Refused {
  let at = self.at()
  self.bump()
  let name = self.until_gap()
  match name {
    "YAML" => {
      self.gap()
      let version = self.until_gap()
      if version.length() < 2 || version[0].to_int() != 0x31 {
        raise BadTag(at, "%YAML " + version)
      }
    }
    "TAG" => {
      self.gap()
      let handle = self.until_gap()
      self.gap()
      let prefix = self.until_gap()
      if handle.length() == 0 || prefix.length() == 0 {
        raise Truncated(at)
      }
      self.handles[handle] = prefix
    }
    _ => raise BadTag(at, "%" + name)
  }
  self.gap()
  self.note()
  let _ = self.brk()
}

///|
/// The run of non-space, non-break characters at the cursor.
fn Reader::until_gap(self : Reader) -> String {
  let start = self.pos
  while !self.eof() &&
        self.cur() != 0x20 &&
        self.cur() != 0x09 &&
        self.cur() != 0x0a &&
        self.cur() != 0x0d {
    self.bump()
  }
  self.src[start:self.pos].to_owned()
}

// -- nodes --------------------------------------------------------------------

///|
/// The properties a node may carry in front of it: an anchor, a tag, or both in
/// either order (§6.9).
priv struct Props {
  anchor : String?
  tag : String?
}

///|
/// Read whatever anchor and tag sit at the cursor.
fn Reader::props(self : Reader) -> Props raise Refused {
  let mut anchor = None
  let mut tag = None
  for ;; {
    if self.cur() == 0x26 {
      let at = self.at()
      self.bump()
      let name = self.until_anchor()
      if name.length() == 0 {
        raise BadAnchor(at, "")
      }
      anchor = Some(name)
    } else if self.cur() == 0x21 {
      tag = Some(self.tag())
    } else {
      break
    }
    // A property may be followed by the node on the same line or the next one.
    let save = (self.pos, self.line, self.bol)
    self.gap()
    self.note()
    if self.brk() {
      self.skip()
      if self.cur() != 0x26 && self.cur() != 0x21 {
        // The node is on a following line; hand the cursor back to the caller
        // positioned on it.
        break
      }
    } else if self.pos == save.0 {
      break
    }
  }
  { anchor, tag, }
}

///|
/// An anchor or alias name: anything up to whitespace or a flow indicator (§6.9.2).
fn Reader::until_anchor(self : Reader) -> String {
  let start = self.pos
  while !self.eof() {
    let c = self.cur()
    if c == 0x20 ||
      c == 0x09 ||
      c == 0x0a ||
      c == 0x0d ||
      c == 0x2c ||
      c == 0x5b ||
      c == 0x5d ||
      c == 0x7b ||
      c == 0x7d {
      break
    }
    self.bump()
  }
  self.src[start:self.pos].to_owned()
}

///|
/// A tag: `!`, `!local`, `!!core`, `!handle!suffix` or `!` (§6.8.2, §6.9.1),
/// resolved through the document's `%TAG` handles to the full tag it names.
fn Reader::tag(self : Reader) -> String raise Refused {
  let at = self.at()
  self.bump()
  if self.cur() == 0x3c {
    self.bump()
    let start = self.pos
    while !self.eof() && self.cur() != 0x3e {
      self.bump()
    }
    if self.eof() {
      raise Truncated(at)
    }
    let verbatim = self.src[start:self.pos].to_owned()
    self.bump()
    return verbatim
  }
  let start = self.pos
  while !self.eof() {
    let c = self.cur()
    if c == 0x20 ||
      c == 0x09 ||
      c == 0x0a ||
      c == 0x0d ||
      c == 0x2c ||
      c == 0x5b ||
      c == 0x5d ||
      c == 0x7b ||
      c == 0x7d {
      break
    }
    self.bump()
  }
  let body = self.src[start:self.pos].to_owned()
  // `!!x` is the secondary handle; `!h!x` a named one; `!x` the primary.
  if body.length() > 0 && body[0].to_int() == 0x21 {
    let suffix = body[1:].to_owned()
    return prefix_of(self.handles, "!!", "tag:yaml.org,2002:") + suffix
  }
  let bang = index_of_unit(body, 0x21)
  if bang >= 0 {
    let handle = "!" + body[0:bang].to_owned() + "!"
    let suffix = body[bang + 1:].to_owned()
    match self.handles.get(handle) {
      Some(p) => return p + suffix
      None => raise BadTag(at, handle)
    }
  }
  prefix_of(self.handles, "!", "!") + body
}

///|
/// The prefix a handle names, or the default when the document did not redefine it.
fn prefix_of(
  handles : Map[String, String],
  handle : String,
  fallback : String,
) -> String {
  match handles.get(handle) {
    Some(p) => p
    None => fallback
  }
}

///|
/// The index of the code unit `u` in `s`, or `-1`.
fn index_of_unit(s : String, u : Int) -> Int {
  for i = 0; i < s.length(); i = i + 1 {
    if s[i].to_int() == u {
      return i
    }
  }
  -1
}

///|
/// A node in block context, everything on it indented further than `indent`.
///
/// `depth` bounds nesting; a document that nests past the flavor's limit is refused
/// rather than driving the reader's own recursion into the ground.
fn Reader::node(self : Reader, indent : Int, depth : Int) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  self.spend(self.at())
  self.gap()
  if self.cur() == 0x2a {
    return self.refer()
  }
  let props = self.props()
  self.gap()
  let value = self.body(indent, depth, props.tag)
  match props.anchor {
    Some(name) => self.anchors[name] = value
    None => ()
  }
  value
}

///|
/// What a node is made of, once its properties are off.
fn Reader::body(
  self : Reader,
  indent : Int,
  depth : Int,
  tag : String?,
) -> Json raise Refused {
  match self.cur() {
    0x5b => self.flow_seq(depth)
    0x7b => self.flow_map(depth)
    0x7c => self.block_scalar(indent, false, tag)
    0x3e => self.block_scalar(indent, true, tag)
    _ => {
      // A `- ` here opens a block sequence; a plain or quoted token followed by
      // `: ` opens a block mapping; anything else is one scalar.
      if self.at_item() {
        return self.block_seq(self.column(), depth)
      }
      if self.at_explicit_key() {
        return self.block_map(self.column(), depth)
      }
      let at = self.at()
      let start = (self.pos, self.line, self.bol)
      let (text, quoted) = self.scalar_text(indent, false)
      if self.at_value_sep() {
        self.pos = start.0
        self.line = start.1
        self.bol = start.2
        return self.block_map(self.column(), depth)
      }
      if self.eof() && !quoted && text.length() == 0 {
        return Json::null()
      }
      resolve(text, quoted, tag, at)
    }
  }
}

///|
/// Whether a block sequence entry starts at the cursor: a `-` that is on its own or
/// followed by a space, rather than the start of a number like `-1`.
fn Reader::at_item(self : Reader) -> Bool {
  self.cur() == 0x2d && self.ahead(1) is (-1 | 0x20 | 0x09 | 0x0a | 0x0d)
}

///|
/// Whether an explicit key indicator `? ` starts at the cursor (§8.2.2).
fn Reader::at_explicit_key(self : Reader) -> Bool {
  self.cur() == 0x3f && self.ahead(1) is (-1 | 0x20 | 0x09 | 0x0a | 0x0d)
}

///|
/// Whether the cursor sits on the `: ` that separates a key from its value.
fn Reader::at_value_sep(self : Reader) -> Bool {
  self.cur() == 0x3a && self.ahead(1) is (-1 | 0x20 | 0x09 | 0x0a | 0x0d)
}

///|
/// An alias `*name`, which is the node its anchor named (§7.1).
///
/// The expansion is counted against the flavor's budget, because an alias may name
/// a node that itself holds aliases and a dozen lines can otherwise expand to
/// billions of them.
fn Reader::refer(self : Reader) -> Json raise Refused {
  let at = self.at()
  self.bump()
  let name = self.until_anchor()
  if !self.flavor.aliases {
    raise BadAnchor(at, name)
  }
  guard self.anchors.get(name) is Some(node) else { raise BadAnchor(at, name) }
  let n = weight(node)
  self.spent = self.spent + n
  if self.spent > self.flavor.budget {
    raise Exceeded(at~, limit=self.flavor.budget, got=self.spent)
  }
  node
}

///|
/// Charge one node against the alias budget, so a document without aliases is
/// bounded too.
fn Reader::spend(self : Reader, at : At) -> Unit raise Refused {
  self.spent = self.spent + 1
  if self.spent > self.flavor.budget {
    raise Exceeded(at~, limit=self.flavor.budget, got=self.spent)
  }
}

///|
/// How many nodes a value holds, counting itself.
fn weight(v : Json) -> Int {
  match v {
    Array(items) => {
      let mut n = 1
      for it in items {
        n = n + weight(it)
      }
      n
    }
    Object(fields) => {
      let mut n = 1
      for _, it in fields {
        n = n + weight(it)
      }
      n
    }
    _ => 1
  }
}

// -- block collections --------------------------------------------------------

///|
/// A block sequence whose `-` indicators sit in column `indent` (§8.2.1).
fn Reader::block_seq(
  self : Reader,
  indent : Int,
  depth : Int,
) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let out : Array[Json] = []
  for ;; {
    self.skip()
    if self.eof() || self.at_marker() || self.at_end_marker() {
      break
    }
    if self.column() != indent || !self.at_item() {
      break
    }
    self.bump()
    // The entry's value may sit on this line, compactly nested, or on the lines
    // under it. Either way it is bounded by this sequence's own indentation.
    self.gap()
    if self.spent_line() {
      self.gap()
      self.note()
      let _ = self.brk()

      self.skip()
      if self.eof() || self.column() <= indent {
        out.push(Json::null())
        continue
      }
      out.push(self.node(indent, depth + 1))
    } else {
      out.push(self.node(indent, depth + 1))
    }
  }
  Json::array(out)
}

///|
/// A block mapping whose keys sit in column `indent` (§8.2.2).
fn Reader::block_map(
  self : Reader,
  indent : Int,
  depth : Int,
) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let out : Map[String, Json] = Map([])
  for ;; {
    self.skip()
    if self.eof() || self.at_marker() || self.at_end_marker() {
      break
    }
    if self.column() != indent {
      break
    }
    let at = self.at()
    let key = if self.at_explicit_key() {
      self.bump()
      self.gap()
      let k = self.node(indent, depth + 1)
      self.skip()
      // The `:` of an explicit pair may be on a line of its own, in column `indent`.
      if self.column() == indent && self.at_value_sep() {
        self.bump()
      }
      key_of(k, at)
    } else {
      if self.at_item() {
        break
      }
      let (text, quoted) = self.scalar_text(indent, true)
      guard self.at_value_sep() else {
        if text.length() == 0 && !quoted {
          break
        }
        raise Unexpected(at, self.printable())
      }
      self.bump()
      key_of(resolve(text, quoted, None, at), at)
    }
    self.gap()
    let value = if self.spent_line() {
      self.gap()
      self.note()
      let _ = self.brk()

      self.skip()
      if self.eof() || self.at_marker() || self.at_end_marker() {
        Json::null()
      } else if self.column() > indent {
        self.node(indent, depth + 1)
      } else if self.column() == indent && self.at_item() {
        // A block sequence may sit in its parent key's own column (§8.2.1), which
        // is how most configuration files in the wild are written.
        self.block_seq(indent, depth + 1)
      } else {
        Json::null()
      }
    } else {
      self.node(indent, depth + 1)
    }
    match (out.get(key), self.flavor.duplicates) {
      (Some(_), First) => ()
      (Some(_), Reject) => raise Repeated(at, key)
      _ => out[key] = value
    }
  }
  Json::object(out)
}

///|
/// A mapping key as the string it keys by.
///
/// YAML allows any node to be a key; `Json` has only string keys, so a collection
/// used as one is refused rather than silently stringified into something no
/// caller could look up.
fn key_of(v : Json, at : At) -> String raise Refused {
  match v {
    String(s) => s
    Number(n, ..) => n.to_string()
    True => "true"
    False => "false"
    Null => "null"
    _ => raise Unexpected(at, '?')
  }
}

///|
/// The character at the cursor, for an error message.
fn Reader::printable(self : Reader) -> Char {
  if self.eof() {
    ' '
  } else {
    self.src[self.pos].unsafe_to_char()
  }
}

// -- flow collections ---------------------------------------------------------

///|
/// A flow sequence `[a, b, c]` (§7.4.1), which may span lines.
fn Reader::flow_seq(self : Reader, depth : Int) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let at = self.at()
  self.bump()
  let out : Array[Json] = []
  for ;; {
    self.skip()
    if self.eof() {
      raise Truncated(at)
    }
    if self.cur() == 0x5d {
      self.bump()
      break
    }
    out.push(self.flow_node(depth + 1))
    self.skip()
    if self.eof() {
      raise Truncated(at)
    }
    if self.cur() == 0x2c {
      self.bump()
    } else if self.cur() != 0x5d {
      raise Unexpected(self.at(), self.printable())
    }
  }
  Json::array(out)
}

///|
/// A flow mapping `{a: 1, b: 2}` (§7.4.2), which may span lines.
fn Reader::flow_map(self : Reader, depth : Int) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let at = self.at()
  self.bump()
  let out : Map[String, Json] = Map([])
  for ;; {
    self.skip()
    if self.eof() {
      raise Truncated(at)
    }
    if self.cur() == 0x7d {
      self.bump()
      break
    }
    let kat = self.at()
    if self.at_explicit_key() {
      self.bump()
      self.gap()
    }
    let key = key_of(self.flow_node(depth + 1), kat)
    self.skip()
    let value = if self.cur() == 0x3a {
      self.bump()
      self.skip()
      if self.cur() == 0x2c || self.cur() == 0x7d {
        Json::null()
      } else {
        self.flow_node(depth + 1)
      }
    } else {
      // `{a, b}` is a set: keys with null values (§7.4.2).
      Json::null()
    }
    match (out.get(key), self.flavor.duplicates) {
      (Some(_), First) => ()
      (Some(_), Reject) => raise Repeated(kat, key)
      _ => out[key] = value
    }
    self.skip()
    if self.eof() {
      raise Truncated(at)
    }
    if self.cur() == 0x2c {
      self.bump()
    } else if self.cur() != 0x7d {
      raise Unexpected(self.at(), self.printable())
    }
  }
  Json::object(out)
}

///|
/// One node inside a flow collection, where indentation carries no meaning and the
/// terminators are the flow indicators.
fn Reader::flow_node(self : Reader, depth : Int) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  self.spend(self.at())
  self.skip()
  if self.cur() == 0x2a {
    return self.refer()
  }
  let props = self.props()
  self.skip()
  let at = self.at()
  let value = match self.cur() {
    0x5b => self.flow_seq(depth)
    0x7b => self.flow_map(depth)
    _ => {
      let (text, quoted) = self.scalar_text(-1, true)
      resolve(text, quoted, props.tag, at)
    }
  }
  match props.anchor {
    Some(name) => self.anchors[name] = value
    None => ()
  }
  value
}

// -- scalars ------------------------------------------------------------------

///|
/// The text of a scalar at the cursor and whether it was quoted, which is what
/// decides between the string `"1"` and the number `1`.
///
/// `flow` says the scalar is being read where a comma or a bracket ends it, which
/// is true inside a flow collection and also of a block mapping's key.
fn Reader::scalar_text(
  self : Reader,
  indent : Int,
  flow : Bool,
) -> (String, Bool) raise Refused {
  match self.cur() {
    0x27 => (self.single(), true)
    0x22 => (self.double(), true)
    _ => (self.plain(indent, flow), false)
  }
}

///|
/// A single-quoted scalar (§7.3.2): the only thing it spells is a quote, doubled.
///
/// This is the form a Windows path or a regular expression is written in, because
/// a backslash in it is a backslash.
fn Reader::single(self : Reader) -> String raise Refused {
  let at = self.at()
  self.bump()
  let out = StringBuilder()
  for ;; {
    if self.eof() {
      raise Truncated(at)
    }
    let c = self.cur()
    if c == 0x27 {
      self.bump()
      if self.cur() == 0x27 {
        out.write_char('\'')
        self.bump()
        continue
      }
      break
    }
    if c == 0x0a || c == 0x0d {
      out.write_string(self.fold(at))
      continue
    }
    out.write_char(self.src[self.pos].unsafe_to_char())
    self.bump()
  }
  out.to_string()
}

///|
/// A double-quoted scalar (§7.3.1): the only form with escapes, and the only one
/// that can spell a character no other form can.
fn Reader::double(self : Reader) -> String raise Refused {
  let at = self.at()
  self.bump()
  let out = StringBuilder()
  for ;; {
    if self.eof() {
      raise Truncated(at)
    }
    let c = self.cur()
    if c == 0x22 {
      self.bump()
      break
    }
    if c == 0x5c {
      let eat = self.at()
      self.bump()
      // A backslash at end of line joins the two lines with no space at all,
      // which is the only way to write a long line with no break in it (§7.3.1).
      if self.cur() == 0x0a || self.cur() == 0x0d {
        let _ = self.brk()

        self.gap()
        continue
      }
      self.escape(out, eat)
      continue
    }
    if c == 0x0a || c == 0x0d {
      out.write_string(self.fold(at))
      continue
    }
    out.write_char(self.src[self.pos].unsafe_to_char())
    self.bump()
  }
  out.to_string()
}

///|
/// One escape past the backslash (§5.7).
fn Reader::escape(
  self : Reader,
  out : StringBuilder,
  at : At,
) -> Unit raise Refused {
  if self.eof() {
    raise Truncated(at)
  }
  let e = self.cur()
  self.bump()
  match e {
    0x30 => out.write_char('\u{00}')
    0x61 => out.write_char('\u{07}')
    0x62 => out.write_char('\u{08}')
    0x74 | 0x09 => out.write_char('\t')
    0x6e => out.write_char('\n')
    0x76 => out.write_char('\u{0b}')
    0x66 => out.write_char('\u{0c}')
    0x72 => out.write_char('\r')
    0x65 => out.write_char('\u{1b}')
    0x20 => out.write_char(' ')
    0x22 => out.write_char('"')
    0x2f => out.write_char('/')
    0x5c => out.write_char('\\')
    0x4e => out.write_char('\u{85}')
    0x5f => out.write_char('\u{a0}')
    0x4c => out.write_char('\u{2028}')
    0x50 => out.write_char('\u{2029}')
    0x78 => self.hex(out, 2, at)
    0x75 => self.hex(out, 4, at)
    0x55 => self.hex(out, 8, at)
    _ => raise BadEscape(at)
  }
}

///|
/// `n` hexadecimal digits as the character they name.
fn Reader::hex(
  self : Reader,
  out : StringBuilder,
  n : Int,
  at : At,
) -> Unit raise Refused {
  let mut v = 0
  for i = 0; i < n; i = i + 1 {
    guard nibble(self.cur()) is Some(d) else { raise BadEscape(at) }
    v = (v << 4) | d
    self.bump()
  }
  // A scalar escape names a code point; anything past the Unicode range, or a lone
  // surrogate, names nothing.
  if v > 0x10ffff || (v >= 0xd800 && v <= 0xdfff) {
    raise BadEscape(at)
  }
  out.write_char(v.unsafe_to_char())
}

///|
/// A hexadecimal digit's value.
fn nibble(c : Int) -> Int? {
  if c >= 0x30 && c <= 0x39 {
    Some(c - 0x30)
  } else if c >= 0x61 && c <= 0x66 {
    Some(c - 0x61 + 10)
  } else if c >= 0x41 && c <= 0x46 {
    Some(c - 0x41 + 10)
  } else {
    None
  }
}

///|
/// The line break inside a quoted or plain scalar, folded (§6.5): one break becomes
/// a space, and each break after the first stays a break.
fn Reader::fold(self : Reader, at : At) -> String raise Refused {
  let mut breaks = 0
  for ;; {
    self.gap()
    if self.brk() {
      breaks = breaks + 1
      continue
    }
    break
  }
  if self.eof() {
    raise Truncated(at)
  }
  if breaks <= 1 {
    " "
  } else {
    let out = StringBuilder()
    for i = 1; i < breaks; i = i + 1 {
      out.write_char('\n')
    }
    out.to_string()
  }
}

///|
/// A plain scalar (§7.3.3): no quotes, so no escapes, and it ends at the first
/// thing that means something else.
///
/// In block context it may run over several lines, each more indented than its
/// parent, and those breaks fold as they do in a quoted scalar. In flow context a
/// comma or a bracket ends it.
fn Reader::plain(self : Reader, indent : Int, flow : Bool) -> String {
  let out = StringBuilder()
  let mut first = true
  for ;; {
    let start = self.pos
    while !self.eof() {
      let c = self.cur()
      if c == 0x0a || c == 0x0d {
        break
      }
      // ` #` begins a comment; a `#` glued to a character is part of the value.
      if c == 0x23 && self.pos > start && is_blank(self.ahead(-1)) {
        break
      }
      if c == 0x23 && self.pos == start && !first {
        break
      }
      // `: ` ends a plain scalar wherever it appears, which is what makes
      // `a: b: c` a fault rather than a guess.
      if c == 0x3a && self.ahead(1) is (-1 | 0x20 | 0x09 | 0x0a | 0x0d) {
        break
      }
      if flow && (c == 0x2c || c == 0x5b || c == 0x5d || c == 0x7b || c == 0x7d) {
        break
      }
      if flow && c == 0x3a {
        break
      }
      self.bump()
    }
    let piece = trim_unit(self.src[start:self.pos].to_owned())
    if piece.length() > 0 {
      if !first {
        out.write_char(' ')
      }
      out.write_string(piece)
      first = false
    }
    // A plain scalar continues onto the next line only in block context, and only
    // while that line is indented past the block it belongs to.
    if flow {
      break
    }
    let save = (self.pos, self.line, self.bol)
    let mut breaks = 0
    for ;; {
      self.gap()
      self.note()
      if self.brk() {
        breaks = breaks + 1
        continue
      }
      break
    }
    if breaks == 0 ||
      self.eof() ||
      self.column() <= indent ||
      self.at_marker() ||
      self.at_end_marker() ||
      self.at_item() ||
      self.opens_pair(indent) {
      self.pos = save.0
      self.line = save.1
      self.bol = save.2
      break
    }
    if breaks > 1 {
      for i = 1; i < breaks; i = i + 1 {
        out.write_char('\n')
      }
      first = true
    }
  }
  out.to_string()
}

///|
/// Whether the line at the cursor is a `key:` line rather than the continuation of
/// a plain scalar, which is what stops a scalar from swallowing the rest of a
/// mapping.
fn Reader::opens_pair(self : Reader, indent : Int) -> Bool {
  let save = (self.pos, self.line, self.bol)
  let mut found = false
  while !self.eof() {
    let c = self.cur()
    if c == 0x0a || c == 0x0d {
      break
    }
    if c == 0x3a && self.ahead(1) is (-1 | 0x20 | 0x09 | 0x0a | 0x0d) {
      found = true
      break
    }
    self.bump()
  }
  self.pos = save.0
  self.line = save.1
  self.bol = save.2
  found && indent >= 0
}

///|
/// Whether a code unit is a space or a tab.
fn is_blank(c : Int) -> Bool {
  c == 0x20 || c == 0x09
}

///|
/// Strip leading and trailing spaces and tabs.
fn trim_unit(s : String) -> String {
  let mut a = 0
  let mut b = s.length()
  while a < b && is_blank(s[a].to_int()) {
    a = a + 1
  }
  while b > a && is_blank(s[b - 1].to_int()) {
    b = b - 1
  }
  s[a:b].to_owned()
}

// -- block scalars ------------------------------------------------------------

///|
/// A literal `|` or folded `>` block scalar (§8.1), with its chomping indicator and
/// optional explicit indentation.
///
/// This is how a shell script or a PEM certificate goes into a configuration file
/// with its line breaks intact, and it is the reason this reader works on
/// characters: the content is defined by exact leading whitespace, which a reader
/// that trimmed lines first has already thrown away.
fn Reader::block_scalar(
  self : Reader,
  indent : Int,
  folded : Bool,
  tag : String?,
) -> Json raise Refused {
  self.bump()
  // `-` strips the trailing breaks, `+` keeps them all, neither keeps one (§8.1.1.2).
  let mut chomp = 0
  let mut explicit = 0
  for ;; {
    let c = self.cur()
    if c == 0x2d {
      chomp = -1
      self.bump()
    } else if c == 0x2b {
      chomp = 1
      self.bump()
    } else if c >= 0x31 && c <= 0x39 {
      explicit = c - 0x30
      self.bump()
    } else {
      break
    }
  }
  self.gap()
  self.note()
  if !self.brk() && !self.eof() {
    raise Unexpected(self.at(), self.printable())
  }
  // The content's indentation is the first non-empty line's, unless the header said
  // otherwise; empty lines before it may be wider and do not set it.
  let mut width = if explicit > 0 { indent + explicit } else { -1 }
  let lines : Array[String] = []
  let mut leading = 0
  for ;; {
    if self.eof() {
      break
    }
    let bol = self.pos
    let mut col = 0
    while self.cur() == 0x20 && (width < 0 || col < width) {
      self.bump()
      col = col + 1
    }
    if self.cur() == 0x0a || self.cur() == 0x0d {
      // An empty line: it belongs to the scalar whatever its width.
      let _ = self.brk()

      if width < 0 {
        leading = leading + 1
      } else {
        lines.push("")
      }
      continue
    }
    if self.eof() {
      break
    }
    if width < 0 {
      if col <= indent {
        self.pos = bol
        break
      }
      width = col
      for i = 0; i < leading; i = i + 1 {
        lines.push("")
      }
      leading = 0
    } else if col < width {
      self.pos = bol
      break
    }
    let start = self.pos
    while !self.eof() && self.cur() != 0x0a && self.cur() != 0x0d {
      self.bump()
    }
    lines.push(self.src[start:self.pos].to_owned())
    let _ = self.brk()
  }
  if width < 0 {
    // Nothing but empty lines: the scalar is however many breaks chomping keeps.
    return Json::string(chomped("", leading, chomp))
  }
  let out = StringBuilder()
  let mut trailing = 0
  for i = 0; i < lines.length(); i = i + 1 {
    if lines[i].length() == 0 {
      trailing = trailing + 1
      continue
    }
    // Everything before this line's content is a break that survived.
    if i > 0 {
      if folded && trailing == 0 {
        // Folding joins two content lines with a space, unless the following one
        // is itself indented further — then it is kept as written (§8.1.3).
        if is_blank(lines[i][0].to_int()) {
          out.write_char('\n')
        } else {
          out.write_char(' ')
        }
      } else {
        for k = 0; k < (if folded { trailing } else { trailing + 1 }); k = k + 1 {
          out.write_char('\n')
        }
        if folded && trailing > 0 {
          // A folded scalar's blank lines are breaks, and the break that ended the
          // previous content line is folded away into them.
          ()
        }
      }
    }
    out.write_string(lines[i])
    trailing = 0
  }
  let body = out.to_string()
  let breaks = if body.length() == 0 { 0 } else { trailing + 1 }
  let _ = tag
  Json::string(chomped(body, breaks, chomp))
}

///|
/// The body with its trailing line breaks as the chomping indicator asks: `-` none,
/// `+` all of them, and neither exactly one (§8.1.1.2).
fn chomped(body : String, breaks : Int, chomp : Int) -> String {
  let keep = match chomp {
    -1 => 0
    1 => breaks
    _ => if breaks > 0 { 1 } else { 0 }
  }
  let out = StringBuilder()
  out.write_string(body)
  for i = 0; i < keep; i = i + 1 {
    out.write_char('\n')
  }
  out.to_string()
}

// -- resolution ---------------------------------------------------------------

///|
/// A scalar as the value it denotes, under the core schema (§10.2) and whatever tag
/// it carried.
///
/// A quoted scalar is always a string: that is the whole point of quoting it, and
/// it is what makes `version: 1.0` a number and `version: "1.0"` a string.
fn resolve(
  text : String,
  quoted : Bool,
  tag : String?,
  at : At,
) -> Json raise Refused {
  match tag {
    Some(t) => return tagged(text, t, at)
    None => ()
  }
  if quoted {
    return Json::string(text)
  }
  untagged(text)
}

///|
/// A scalar with no tag, resolved by what it looks like.
fn untagged(text : String) -> Json {
  if text.length() == 0 || text == "~" {
    return Json::null()
  }
  match text {
    "null" | "Null" | "NULL" => return Json::null()
    "true" | "True" | "TRUE" => return Json::boolean(true)
    "false" | "False" | "FALSE" => return Json::boolean(false)
    _ => ()
  }
  match number(text) {
    Some(n) => Json::number(n)
    None => Json::string(text)
  }
}

///|
/// A scalar under an explicit tag.
///
/// The core schema's tags are honoured; a tag this reader does not know is refused
/// rather than dropped, because a document that asked for a type it did not get has
/// not been read.
fn tagged(text : String, tag : String, at : At) -> Json raise Refused {
  match tag {
    "tag:yaml.org,2002:str" => Json::string(text)
    "tag:yaml.org,2002:null" =>
      if text.length() == 0 ||
        text == "~" ||
        text == "null" ||
        text == "Null" ||
        text == "NULL" {
        Json::null()
      } else {
        raise Unexpected(at, '~')
      }
    "tag:yaml.org,2002:bool" =>
      match text {
        "true" | "True" | "TRUE" | "yes" | "Yes" | "YES" | "on" | "On" | "ON" =>
          Json::boolean(true)
        "false"
        | "False"
        | "FALSE"
        | "no"
        | "No"
        | "NO"
        | "off"
        | "Off"
        | "OFF" => Json::boolean(false)
        _ => raise Unexpected(at, '?')
      }
    "tag:yaml.org,2002:int" | "tag:yaml.org,2002:float" =>
      match number(text) {
        Some(n) => Json::number(n)
        None => raise Unexpected(at, '?')
      }
    // The non-specific tags say "whatever this looks like" and "a string".
    "!" => Json::string(text)
    "?" => untagged(text)
    _ => raise BadTag(at, tag)
  }
}

///|
/// A numeric literal under the core schema (§10.2.1.2, §10.2.1.3), or `None` for
/// anything that is not wholly numeric so the caller falls back to a string.
///
/// The core schema's integers are decimal, `0o` octal and `0x` hexadecimal; its
/// floats carry an optional exponent, and `.inf` and `.nan` in either case.
fn number(s : String) -> Double? {
  if s.length() == 0 {
    return None
  }
  let mut i = 0
  let mut sign = 1.0
  if s[0].to_int() == 0x2d {
    sign = -1.0
    i = 1
  } else if s[0].to_int() == 0x2b {
    i = 1
  }
  if i >= s.length() {
    return None
  }
  let rest = s[i:].to_owned()
  match rest {
    ".inf" | ".Inf" | ".INF" => return Some(sign * @double.infinity)
    _ => ()
  }
  if i == 0 {
    match rest {
      ".nan" | ".NaN" | ".NAN" => return Some(@double.not_a_number)
      _ => ()
    }
  }
  // `0o` and `0x` are whole integers or nothing.
  if rest.length() > 2 && rest[0].to_int() == 0x30 {
    let radix = match rest[1].to_int() {
      0x6f => 8
      0x78 => 16
      _ => 0
    }
    if radix != 0 {
      let mut acc = 0.0
      for k = 2; k < rest.length(); k = k + 1 {
        guard nibble(rest[k].to_int()) is Some(d) else { return None }
        if d >= radix {
          return None
        }
        acc = acc * radix.to_double() + d.to_double()
      }
      return Some(sign * acc)
    }
  }
  let mut whole = 0.0
  let mut digits = false
  while i < s.length() && s[i].to_int() >= 0x30 && s[i].to_int() <= 0x39 {
    whole = whole * 10.0 + (s[i].to_int() - 0x30).to_double()
    digits = true
    i = i + 1
  }
  let mut value = whole
  if i < s.length() && s[i].to_int() == 0x2e {
    i = i + 1
    let mut frac = 0.0
    let mut scale = 1.0
    while i < s.length() && s[i].to_int() >= 0x30 && s[i].to_int() <= 0x39 {
      frac = frac * 10.0 + (s[i].to_int() - 0x30).to_double()
      scale = scale * 10.0
      digits = true
      i = i + 1
    }
    value = value + frac / scale
  }
  if !digits {
    return None
  }
  if i < s.length() && (s[i].to_int() == 0x65 || s[i].to_int() == 0x45) {
    i = i + 1
    let mut esign = 1
    if i < s.length() && (s[i].to_int() == 0x2b || s[i].to_int() == 0x2d) {
      if s[i].to_int() == 0x2d {
        esign = -1
      }
      i = i + 1
    }
    let mut exp = 0
    let mut any = false
    while i < s.length() && s[i].to_int() >= 0x30 && s[i].to_int() <= 0x39 {
      exp = exp * 10 + (s[i].to_int() - 0x30)
      any = true
      i = i + 1
    }
    if !any {
      return None
    }
    let mut factor = 1.0
    for k = 0; k < exp; k = k + 1 {
      factor = factor * 10.0
    }
    value = if esign < 0 { value / factor } else { value * factor }
  }
  if i != s.length() {
    return None
  }
  Some(sign * value)
}