// 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 : Int32
  mut line : Int32
  // Offset of the first character of the current line, so a column is a subtraction.
  mut bol : Int32
  flavor : Flavor
  // Which YAML version is reading: what a scalar resolves to, and what is
  // allowed besides. See `Schema`.
  schema : Schema
  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 : Int32
  // Whether a document may begin here without a `---`. True at the start of the
  // stream and after a `...`; false once a root node has been read, which is what
  // makes a stray line after a document a fault rather than a second document.
  mut fresh : Bool
  // Whether this document has already carried a `%YAML` directive (§6.8.1 allows
  // one).
  mut versioned : Bool
}

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

// -- 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) -> Int32 {
  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 : Int32) -> Int32 {
  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) -> Int32 {
  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()
  }
}

///|
/// Whether a comment starts at the cursor.
///
/// A `#` only begins one at the start of a line or after a space (§6.2), so
/// `"value"#` is not a value with a comment after it but a fault — and a `#`
/// inside `foo#bar` is part of the scalar.
fn Reader::at_note(self : Reader) -> Bool {
  self.cur() == 0x23 && (self.pos == self.bol || is_blank(self.ahead(-1)))
}

///|
/// Skip a comment, if one starts here, up to but not including the line break.
fn Reader::note(self : Reader) -> Unit {
  if self.at_note() {
    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.at_note() ||
    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
    }
    // Content that is neither a marker nor a directive, where a document may not
    // begin, is a document that did not stop where it said it did.
    if !self.fresh && !self.at_marker() {
      raise Trailing(self.at())
    }
    match self.document() {
      Some(root) => out.push(root)
      None => ()
    }
  }
  out
}

///|
/// One document: its directives, an optional `---`, its root node, and an optional
/// `...` (§9.1.2, §9.1.3).
///
/// `None` where the stream held nothing to read — a `...` with no document before
/// it ends an empty stream, and an empty stream has no documents.
fn Reader::document(self : Reader) -> Json? raise Refused {
  self.handles.clear()
  self.anchors.clear()
  self.fresh = false
  self.versioned = false
  let mut marked = false
  let mut directed = false
  let mut inline_root = false
  for ;; {
    self.skip()
    if self.cur() == 0x25 {
      self.directive()
      directed = true
      continue
    }
    if self.marker("---") {
      marked = true
      // A node may follow on the same line — `--- |` opens a block scalar there,
      // but a block collection may not start there (§9.1.2): its keys would have
      // nowhere to line up.
      self.gap()
      inline_root = !(self.eof() || self.at_note() || self.at_break())
    }
    break
  }
  if self.cur() == 0x25 {
    // A directive may not follow the document it would belong to (§6.8.1).
    raise Trailing(self.at())
  }
  self.skip()
  if self.cur() == 0x25 {
    // A directive belongs before the document, not inside it (§6.8.1), and `%`
    // is a reserved indicator that may not begin a plain scalar (§5.3).
    raise Trailing(self.at())
  }
  if directed && !marked && (self.eof() || self.at_end_marker()) {
    // Directives without a document are a fault: a preamble to nothing.
    raise Truncated(self.at())
  }
  // Nothing at all to read: not a marker, not a directive, not a node.
  let empty = self.eof() || self.at_end_marker() || (marked && self.at_marker())
  let root = if empty {
    Json::null()
  } else {
    self.node(-1, 0, inline=inline_root)
  }
  self.skip()
  let mut ended = false
  if self.at_end_marker() {
    let _ = self.marker("...")
    // `...` ends the document and the line it is on (§9.1.1).
    self.gap()
    if !(self.eof() || self.at_note() || self.at_break()) {
      raise Unexpected(self.at(), self.printable())
    }
    self.note()
    let _ = self.brk()
    ended = true
  }
  if empty && !marked && !directed {
    // A `...` with no document before it ends an empty stream, and a stream may
    // begin again after it.
    self.fresh = ended
    return None
  }
  self.fresh = ended
  Some(root)
}

///|
/// Whether the cursor is on a line break.
fn Reader::at_break(self : Reader) -> Bool {
  self.cur() == 0x0a || self.cur() == 0x0d
}

///|
/// 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. A directive name this
/// reader does not know is skipped, which §6.8.1 also requires — it is reserved for
/// a later revision, not a fault.
fn Reader::directive(self : Reader) -> Unit raise Refused {
  let at = self.at()
  self.bump()
  let name = self.until_gap()
  let mut known = true
  match name {
    "YAML" => {
      if self.versioned {
        raise Unexpected(at, '%')
      }
      self.versioned = true
      self.gap()
      let version = self.until_gap()
      match version_of(version) {
        Some((major, _)) =>
          if major != 1 {
            raise BadTag(at, "%YAML " + version)
          }
        None => raise BadTag(at, "%YAML " + version)
      }
    }
    "TAG" => {
      self.gap()
      let handle = self.until_gap()
      self.gap()
      let prefix = self.until_gap()
      if !is_handle(handle) || prefix.length() == 0 {
        raise BadTag(at, "%TAG " + handle)
      }
      self.handles[handle] = prefix
    }
    _ =>
      // A directive this reader does not know is ignored, with its parameters and
      // its comment, and the document is read as if it were not there (§6.8.1).
      known = false
  }
  if known {
    self.gap()
    if !(self.eof() || self.at_note() || self.at_break()) {
      raise Unexpected(self.at(), self.printable())
    }
  } else {
    // The rest of the line, whatever it says, is this directive's parameter.
    while !self.eof() && self.cur() != 0x0a && self.cur() != 0x0d {
      self.bump()
    }
  }
  self.note()
  let _ = self.brk()
}

///|
/// Whether a `%TAG` handle has the shape §6.8.2.2 gives it: `!`, `!!`, or `!name!`.
fn is_handle(handle : String) -> Bool {
  if handle.length() == 0 || handle[0].to_int() != 0x21 {
    return false
  }
  if handle == "!" || handle == "!!" {
    return true
  }
  handle[handle.length() - 1].to_int() == 0x21 &&
  index_of_unit(handle[1:handle.length() - 1].to_owned(), 0x21) < 0
}

///|
/// 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,
  indent : Int32,
  dedent~ : Bool = false,
) -> Props raise Refused {
  let mut anchor = None
  let mut tag = None
  let mut moved = false
  for ;; {
    if self.cur() == 0x26 {
      let at = self.at()
      if !self.schema.anchors {
        raise Disallowed(at, "an anchor")
      }
      if anchor is Some(_) {
        // A node has one anchor and one tag, at most (§6.9.1). A second one on a
        // line of its own is not this node's: it belongs to the node's content,
        // which in block context is a mapping whose first key is anchored.
        if moved {
          break
        }
        raise Unexpected(at, '&')
      }
      self.bump()
      let name = self.until_anchor()
      if name.length() == 0 {
        raise BadAnchor(at, "")
      }
      anchor = Some(name)
    } else if self.cur() == 0x21 {
      let at = self.at()
      if !self.schema.tags {
        raise Disallowed(at, "a tag")
      }
      if tag is Some(_) {
        if moved {
          break
        }
        raise Unexpected(at, '!')
      }
      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()
      // The node is on a following line. If that line is not indented past the
      // block this node belongs to, there is no node: the property stands for an
      // empty one, and the cursor goes back to the break so the caller can see
      // the rest of the line for what it is.
      //
      // The exception is a sequence entry in the column of the key that holds it:
      // `key: &a` then `- one` is legal (§8.2.1), and `dedent` is how the caller
      // that knows it is holding a value says so.
      let col = self.column()
      let stops = self.eof() ||
        self.at_marker() ||
        self.at_end_marker() ||
        col < indent ||
        (col == indent && !(dedent && self.at_item()))
      if stops {
        self.pos = save.0
        self.line = save.1
        self.bol = save.2
        break
      }
      moved = true
      // Another property, on its own line, is part of this node's properties.
      if self.cur() != 0x26 && self.cur() != 0x21 {
        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, "!!", self.schema.secondary) + 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, "!", self.schema.primary) + 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 : Int32) -> Int32 {
  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 : Int32,
  depth : Int32,
  inline~ : Bool = false,
  dedent~ : Bool = false,
) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  self.spend(self.at())
  self.gap()
  // Where the node begins, properties included: a mapping keyed by this node
  // starts in the column the node starts in, not where its tag or anchor ended.
  if self.cur() == 0x2a {
    if !self.schema.anchors {
      raise Disallowed(self.at(), "an alias")
    }
    // An alias may be the key of the mapping this node is — `*a : 1` — in which
    // case the mapping starts where the alias does, not where its parent does.
    if self.alias_line() {
      return self.block_map(self.column(), depth)
    }
    return self.refer()
  }
  let start = (self.pos, self.line, self.bol)
  let opened = self.column()
  let props = self.props(indent, dedent~)
  // A property may sit on a line of its own, but the node it belongs to still
  // has to be indented past the block that contains it (§6.9).
  let after = self.line
  let moved = after != start.1
  // A mapping keyed by this node starts in the column this node starts in: the
  // column of its first character, which is a property when one is written there.
  let col = if moved { self.column() } else { opened }
  // A node that moved to its own line is no longer inline: nothing on that line
  // came before it.
  let value = self.body(
    indent,
    depth,
    props.tag,
    col,
    start,
    inline && !moved,
    moved,
  )
  match props.anchor {
    Some(name) => self.anchors[name] = value
    None => ()
  }
  value
}

///|
/// A scalar as this version's schema says it is.
fn Reader::resolve(
  self : Reader,
  text : String,
  quoted : Bool,
  tag : String?,
  at : At,
) -> Json raise Refused {
  (self.schema.resolve)(text, quoted, tag, at, self.schema.rules)
}

///|
/// What a node is made of, once its properties are off.
///
/// `col` is the column the node started in, `start` where its first character was,
/// and `inline` says the node follows something else on its line — after a `key:`,
/// where a block collection may not begin.
fn Reader::body(
  self : Reader,
  indent : Int32,
  depth : Int32,
  tag : String?,
  col : Int32,
  start : (Int32, Int32, Int32),
  inline : Bool,
  moved : Bool,
) -> Json raise Refused {
  match self.cur() {
    0x2a => {
      // A property on the line above binds to the mapping whose first key is the
      // alias: `top: &a` and then `  *x : v`. A property written in front of an
      // alias on the alias's own line is a fault, because an alias cannot be
      // anchored or tagged (§7.1).
      if moved && self.alias_line() {
        return self.block_map(self.column(), depth)
      }
      raise Unexpected(self.at(), '*')
    }
    0x26 | 0x21 =>
      // A property that is not this node's, on a line of its own: the mapping
      // whose first key carries it is the node.
      if self.key_line() {
        return self.block_map(self.column(), depth)
      } else {
        raise Unexpected(self.at(), self.printable())
      }
    0x5b | 0x7b => {
      // Flow style is a YAML feature some versions do without: StrictYAML reads
      // a bracket as a plain scalar character and nothing else.
      if !self.schema.flow {
        raise Disallowed(self.at(), "flow style")
      }
      if self.cur() == 0x5b {
        self.flow_seq(indent, depth)
      } else {
        self.flow_map(indent, 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() {
        if inline {
          // A block sequence may not begin on the line of the key that holds it.
          raise Unexpected(self.at(), '-')
        }
        return self.block_seq(col, depth)
      }
      if self.at_explicit_key() {
        if inline {
          raise Unexpected(self.at(), '?')
        }
        return self.block_map(col, depth)
      }
      let at = self.at()
      let own = (self.pos, self.line, self.bol)
      let own_col = self.column()
      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 {
          // A block mapping may not begin on the line of the key that holds it.
          raise Unexpected(self.at(), ':')
        }
        // Back to where this pair began: a property written in front of a key is
        // part of the mapping, not part of the key.
        let back = if moved { own } else { start }
        self.pos = back.0
        self.line = back.1
        self.bol = back.2
        return self.block_map(if moved { own_col } else { col }, depth)
      }
      self.pos = tail.0
      self.line = tail.1
      self.bol = tail.2
      if self.eof() && !quoted && text.length() == 0 {
        return Json::null()
      }
      self.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 a code unit separates tokens: a blank, a break, or a flow indicator.
fn ends_token(c : Int32) -> Bool {
  c is (0x20 | 0x09 | 0x0a | 0x0d | 0x2c | 0x5b | 0x5d | 0x7b | 0x7d | -1)
}

///|
/// Whether an alias at the cursor is the key of a pair: `*a : 1`.
///
/// A name may hold a `:` of its own — `&a:b` names a node `a:b` — so the name is
/// read first and only a `:` *after* it separates the key from its value. That is
/// why `*a:1` is one name and `*a : 1` is a pair (§7.1).
fn Reader::alias_line(self : Reader) -> Bool {
  if self.cur() != 0x2a {
    return false
  }
  let n = self.src.length()
  let mut i = self.pos + 1
  while i < n && !ends_token(self.src[i].to_int()) {
    i = i + 1
  }
  // Blanks may separate the name from the colon; a break may not, because a key
  // is one line long.
  while i < n && (self.src[i].to_int() == 0x20 || self.src[i].to_int() == 0x09) {
    i = i + 1
  }
  if !(i < n && self.src[i].to_int() == 0x3a) {
    return false
  }
  let next = if i + 1 < n { self.src[i + 1].to_int() } else { -1 }
  next is (0x20 | 0x09 | 0x0a | 0x0d | -1)
}

///|
/// Whether the cursor sits at the start of a line holding one `key: value` pair.
///
/// A lookahead, which does not move the cursor. The reader has to ask it when a
/// node turns out to carry properties that belong to its first key rather than to
/// itself: `&node` on one line and `&key k: v` on the next is a mapping whose key
/// is anchored, and nothing else.
fn Reader::key_line(self : Reader) -> Bool {
  let n = self.src.length()
  let mut i = self.pos
  // The properties a key may carry, if any.
  for ;; {
    while i < n &&
          (self.src[i].to_int() == 0x20 || self.src[i].to_int() == 0x09) {
      i = i + 1
    }
    if i < n && self.src[i].to_int() == 0x26 {
      i = i + 1
      while i < n && !ends_token(self.src[i].to_int()) {
        i = i + 1
      }
      continue
    }
    if i < n && self.src[i].to_int() == 0x21 {
      if i + 1 < n && self.src[i + 1].to_int() == 0x3c {
        i = i + 2
        while i < n &&
              self.src[i].to_int() != 0x3e &&
              self.src[i].to_int() != 0x0a {
          i = i + 1
        }
        if i < n && self.src[i].to_int() == 0x3e {
          i = i + 1
        }
      } else {
        i = i + 1
        while i < n && !ends_token(self.src[i].to_int()) {
          i = i + 1
        }
      }
      continue
    }
    break
  }
  // A quoted key is one line long, and may be followed by blanks and a `:`.
  if i < n && (self.src[i].to_int() == 0x22 || self.src[i].to_int() == 0x27) {
    let quote = self.src[i].to_int()
    i = i + 1
    let mut closed = false
    while i < n && !closed {
      let c = self.src[i].to_int()
      if c == 0x0a || c == 0x0d {
        return false
      }
      i = i + 1
      if c == 0x5c && quote == 0x22 {
        i = i + 1
      } else if c == quote {
        if quote == 0x27 && i < n && self.src[i].to_int() == 0x27 {
          i = i + 1
        } else {
          closed = true
        }
      }
    }
    if !closed {
      return false
    }
    while i < n &&
          (self.src[i].to_int() == 0x20 || self.src[i].to_int() == 0x09) {
      i = i + 1
    }
    return i < n && self.src[i].to_int() == 0x3a
  }
  // A plain key ends at a `:` that a blank or the end of the line follows. A
  // colon anywhere else is part of the key, which is how `http://a: 1` reads.
  let mut any = false
  while i < n {
    let c = self.src[i].to_int()
    if c == 0x0a || c == 0x0d {
      return false
    }
    let next = if i + 1 < n { self.src[i + 1].to_int() } else { -1 }
    if c == 0x3a && any && next is (0x20 | 0x09 | 0x0a | 0x0d | -1) {
      return true
    }
    if c == 0x23 && any && self.src[i - 1].to_int() is (0x20 | 0x09) {
      return false
    }
    any = true
    i = i + 1
  }
  false
}

///|
/// 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) -> Int32 {
  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 : Int32,
  depth : Int32,
) -> 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() {
      if self.column() > indent && self.tabbed() {
        raise BadIndent(self.at())
      }
      break
    }
    if self.tabbed() {
      raise BadIndent(self.at())
    }
    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 : Int32,
  depth : Int32,
) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let out : Map[String, Json] = Map([])
  // What a `<<` key named, and where, held until the mapping is complete: an
  // explicit key is written after the merge that names it and still wins over
  // it, so the merge cannot be applied where it is read (YAML 1.1 §7.1).
  let merges : Array[(At, Json)] = []
  for ;; {
    self.skip()
    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()
    let mut key = ""
    let mut value = Json::null()
    if self.at_explicit_key() {
      self.bump()
      self.gap()
      // An explicit key may be empty: `?` alone names the null key (§8.2.2).
      let k = if self.spent_line() {
        Json::null()
      } else {
        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()
        self.gap()
        // After an explicit `:` a block collection may begin on the same line —
        // `: - one` — which an implicit pair may not do (§8.2.2).
        value = if self.spent_line() {
          self.pair_value(indent, depth)
        } else {
          self.node(indent, depth + 1, dedent=true)
        }
      }
      key = key_of(k, at)
    } else {
      if self.at_item() {
        break
      }
      // A key may carry a tag or an anchor of its own (§6.9).
      let kp = self.props(indent)
      self.gap()
      if self.cur() == 0x2a {
        // An alias may name a key — it names a node, and a node may be one — but
        // it is not itself a node, so it cannot carry properties.
        if kp.anchor is Some(_) || kp.tag is Some(_) {
          raise Unexpected(self.at(), '*')
        }
        let k = self.refer()
        self.gap()
        guard self.at_value_sep() else {
          raise Unexpected(at, self.printable())
        }
        self.bump()
        key = key_of(k, at)
        value = self.pair_value(indent, depth)
      } else {
        let kline = self.line
        let (text, quoted) = self.scalar_text(Mode::Key, indent)
        if self.line != kline {
          // A key is one line; a quoted one that ran on is a fault (§7.4.2).
          raise Unexpected(self.at(), '\n')
        }
        // A quoted key keeps the space between it and its `:`, which a plain one
        // has already eaten as part of the token (§7.4.2).
        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())
        }
        self.bump()
        let k = self.resolve(text, quoted, kp.tag, at)
        match kp.anchor {
          Some(name) => self.anchors[name] = k
          None => ()
        }
        key = key_of(k, at)
        // A bare `<<` is the merge key in the versions that have one; in the
        // ones that do not, it is the string it looks like.
        if self.schema.merge && !quoted && text == "<<" {
          merges.push((at, self.pair_value(indent, depth)))
          continue
        }
        value = self.pair_value(indent, depth)
      }
    }
    match (out.get(key), self.flavor.duplicates) {
      (Some(_), First) => ()
      (Some(_), Reject) => raise Repeated(at, key)
      _ => out[key] = value
    }
  }
  Json::object(merged(out, merges))
}

///|
/// The value of a pair whose `:` is behind the cursor: on this line after the
/// gap, or on the lines under it, or nothing at all.
fn Reader::pair_value(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> Json 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() {
      Json::null()
    } else if self.column() > indent {
      self.node(indent, depth + 1, dedent=true)
    } 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, inline=true, dedent=true)
  }
}

///|
/// The mapping, with its merge sources folded in.
///
/// Order is part of a document: PyYAML and ruamel.yaml both put the merged keys
/// where the merge key was, with an explicit key that repeats one overriding the
/// value without moving it. Doing the same here means a document read and written
/// back keeps the shape its author wrote.
fn merged(
  out : Map[String, Json],
  merges : Array[(At, Json)],
) -> Map[String, Json] raise Refused {
  if merges.length() == 0 {
    return out
  }
  let folded = Map([])
  for (at, source) in merges {
    merge_into(folded, source, at)
  }
  let joined = Map([])
  for k, v in folded {
    joined[k] = match out.get(k) {
      Some(explicit) => explicit
      None => v
    }
  }
  for k, v in out {
    if !joined.contains(k) {
      joined[k] = v
    }
  }
  joined
}

///|
/// A merge source folded into the mapping around it (YAML 1.1 §7.1): a mapping's
/// pairs, or a sequence of mappings taken in order. A key already in the
/// mapping is left alone — that is what makes an explicit key win.
fn merge_into(
  out : Map[String, Json],
  source : Json,
  at : At,
) -> Unit raise Refused {
  match source {
    Object(fields) =>
      for k, v in fields {
        if !out.contains(k) {
          out[k] = v
        }
      }
    Array(items) =>
      for item in items {
        merge_into(out, item, at)
      }
    _ => raise Unexpected(at, '?')
  }
}

///|
/// 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,
  indent : Int32,
  depth : Int32,
) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let at = self.at()
  self.bump()
  let out : Array[Json] = []
  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 {
      // An entry is never empty: `[, a]` and `[a, , b]` are both faults (§7.4.1).
      raise Unexpected(self.at(), ',')
    }
    if self.at_explicit_key() {
      // `? key : value` names its key explicitly, which is how a single pair may
      // have a key that runs over a line (§7.4.4).
      let kat = self.at()
      self.bump()
      self.flow_gap(indent, 0x5d)
      let key = if self.cur() is (0x3a | 0x2c | 0x5d) {
        Json::null()
      } else {
        self.flow_node(indent, depth + 1)
      }
      self.flow_gap(indent, 0x5d)
      let value = if self.cur() == 0x3a {
        self.bump()
        self.flow_gap(indent, 0x5d)
        if self.cur() is (0x2c | 0x5d) {
          Json::null()
        } else {
          self.flow_node(indent, depth + 1)
        }
      } else {
        Json::null()
      }
      let pair : Map[String, Json] = Map([(key_of(key, kat), value)])
      out.push(Json::object(pair))
      self.flow_gap(indent, 0x5d)
    } else {
      let kline = self.line
      let mut entry = self.flow_node(indent, depth + 1)
      self.flow_gap(indent, 0x5d)
      if self.cur() == 0x3a {
        if self.line != kline {
          // An implicit key is one line long: a `:` on the line under one is a
          // fault, not a mapping the reader should guess at (§7.4.1).
          raise Unexpected(self.at(), ':')
        }
        // `[a: b]` is a one-pair mapping, which is where a block mapping would
        // have been if the sequence were not flow (§7.4.1).
        let kat = self.at()
        self.bump()
        self.flow_gap(indent, 0x5d)
        let value = if self.cur() == 0x2c || self.cur() == 0x5d {
          Json::null()
        } else {
          self.flow_node(indent, depth + 1)
        }
        let pair : Map[String, Json] = Map([(key_of(entry, kat), value)])
        entry = Json::object(pair)
        self.flow_gap(indent, 0x5d)
      }
      out.push(entry)
    }
    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 document marker where a flow collection is still open is a fault: `---` and
/// `...` only mean anything at the start of a line, in block context (§9.1.1).
fn Reader::no_marker(self : Reader, at : At) -> Unit raise Refused {
  if self.at_marker() || self.at_end_marker() {
    raise Unexpected(at, '-')
  }
}

///|
/// A flow mapping `{a: 1, b: 2}` (§7.4.2), which may span lines.
fn Reader::flow_map(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> Json raise Refused {
  if depth > self.flavor.depth {
    raise TooDeep(self.at())
  }
  let at = self.at()
  self.bump()
  let out : Map[String, Json] = Map([])
  let merges : Array[(At, Json)] = []
  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(), ',')
    }
    // A bare `<<` is the merge key in the versions that have one; a quoted one
    // is the string it spells.
    if self.schema.merge && self.cur() == 0x3c && self.ahead(1) == 0x3c {
      let mat = self.at()
      self.bump()
      self.bump()
      self.flow_gap(indent, 0x7d)
      if self.cur() != 0x3a {
        raise Unexpected(self.at(), self.printable())
      }
      self.bump()
      self.flow_gap(indent, 0x7d)
      let value = if self.cur() == 0x2c || self.cur() == 0x7d {
        Json::null()
      } else {
        self.flow_node(indent, depth + 1)
      }
      merges.push((mat, value))
    } else {
      let kat = self.at()
      if self.at_explicit_key() {
        self.bump()
        self.gap()
      }
      let key = key_of(self.flow_node(indent, depth + 1), kat)
      self.flow_gap(indent, 0x7d)
      let value = if self.cur() == 0x3a {
        self.bump()
        self.flow_gap(indent, 0x7d)
        if self.cur() == 0x2c || self.cur() == 0x7d {
          Json::null()
        } else {
          self.flow_node(indent, 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.flow_gap(indent, 0x7d)
    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(merged(out, merges))
}

///|
/// Skip to the next token inside a flow collection, where a line break may not
/// take the content back under the block that holds the collection (§7.4.1).
///
/// The closing bracket is exempt: it may sit in the column of the key that opened
/// the collection, which is how most files in the wild are written.
fn Reader::flow_gap(
  self : Reader,
  indent : Int32,
  close : Int32,
) -> Unit raise Refused {
  let line = self.line
  self.skip()
  if self.line != line &&
    !self.eof() &&
    self.column() <= indent &&
    self.cur() != close {
    raise BadIndent(self.at())
  }
}

///|
/// One node inside a flow collection, where indentation carries no meaning and the
/// terminators are the flow indicators.
fn Reader::flow_node(
  self : Reader,
  indent : Int32,
  depth : Int32,
) -> 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(-1)
  self.skip()
  let at = self.at()
  let value = match self.cur() {
    0x5b =>
      if !self.schema.flow {
        raise Disallowed(self.at(), "flow style")
      } else {
        self.flow_seq(indent, depth)
      }
    0x7b =>
      if !self.schema.flow {
        raise Disallowed(self.at(), "flow style")
      } else {
        self.flow_map(indent, depth)
      }
    _ => {
      // A property with no node behind it stands for the empty scalar: `{foo: !!str,}`
      // is a mapping whose first value is the empty string (§7.4.2).
      let empty = (props.tag is Some(_) || props.anchor is Some(_)) &&
        self.cur() is (0x2c | 0x5b | 0x5d | 0x7b | 0x7d | 0x3a | -1)
      if empty {
        self.resolve("", false, props.tag, at)
      } else {
        let (text, quoted) = self.scalar_text(Mode::Flow, -1)
        self.resolve(text, quoted, props.tag, at)
      }
    }
  }
  match props.anchor {
    Some(name) => self.anchors[name] = value
    None => ()
  }
  value
}

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

///|
/// Where a scalar is being read, which decides what ends it.
///
/// A `,` ends a scalar inside a flow collection and is an ordinary character in a
/// block one; a bare `[` likewise. A mapping key is one line and ends at its `: `,
/// which is neither of the other two.
priv enum Mode {
  Block
  Flow
  Key
}

///|
/// 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`.
fn Reader::scalar_text(
  self : Reader,
  mode : Mode,
  indent : Int32,
) -> (String, Bool) raise Refused {
  if self.cur() == 0x23 {
    // `#` never begins a scalar: it is a comment, or it is a fault (§6.2).
    raise Unexpected(self.at(), '#')
  }
  if self.cur() is (0x2c | 0x5d | 0x7d) {
    // Nor does a flow indicator, which only ever closes something (§7.3.3).
    raise Unexpected(self.at(), self.printable())
  }
  if mode is Flow &&
    self.cur() == 0x2d &&
    self.ahead(1) is (-1 | 0x2c | 0x5d | 0x7d) {
    // `[-]` is not a dash: a `-` followed by nothing is an indicator in search of
    // an entry.
    raise Unexpected(self.at(), '-')
  }
  match self.cur() {
    0x27 => (self.single(indent), true)
    0x22 => (self.double(indent), true)
    _ => (self.plain(mode, indent), 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, indent : Int32) -> String raise Refused {
  let at = self.at()
  self.bump()
  let out = StringBuilder()
  // Blanks are held back until something follows them: at the end of a line they
  // are not content, and §6.5 drops them.
  let mut blanks = ""
  for ;; {
    if self.eof() {
      raise Truncated(at)
    }
    let c = self.cur()
    if c == 0x27 {
      out.write_string(blanks)
      self.bump()
      if self.cur() == 0x27 {
        out.write_char('\'')
        self.bump()
        blanks = ""
        continue
      }
      break
    }
    if c == 0x0a || c == 0x0d {
      blanks = ""
      out.write_string(self.fold(at, indent))
      continue
    }
    if is_blank(c) {
      blanks = blanks + (if c == 0x20 { " " } else { "\t" })
      self.bump()
      continue
    }
    out.write_string(blanks)
    blanks = ""
    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, indent : Int32) -> String raise Refused {
  let at = self.at()
  self.bump()
  let out = StringBuilder()
  let mut blanks = ""
  for ;; {
    if self.eof() {
      raise Truncated(at)
    }
    let c = self.cur()
    if c == 0x22 {
      out.write_string(blanks)
      self.bump()
      break
    }
    if c == 0x5c {
      out.write_string(blanks)
      blanks = ""
      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 {
      blanks = ""
      out.write_string(self.fold(at, indent))
      continue
    }
    if is_blank(c) {
      blanks = blanks + (if c == 0x20 { " " } else { "\t" })
      self.bump()
      continue
    }
    out.write_string(blanks)
    blanks = ""
    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 : Int32,
  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 : Int32) -> Int32? {
  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, indent : Int32) -> String raise Refused {
  let mut breaks = 0
  for ;; {
    // A tab is separation here, not indentation: it is skipped with the blanks,
    // which is what every reader in the field does with a document that puts one
    // at the start of a continuation line (§6.1 says it should be refused).
    self.gap()
    if self.brk() {
      breaks = breaks + 1
      continue
    }
    break
  }
  if self.eof() {
    raise Truncated(at)
  }
  if breaks > 0 && self.column() <= indent {
    // A quoted scalar that runs on is a continuation of the node it belongs to, so
    // it has to be indented past that node's block (§7.3.1).
    raise BadIndent(self.at())
  }
  if self.at_marker() || self.at_end_marker() {
    // A quoted scalar may not swallow a document marker (§9.1.1).
    raise Unexpected(self.at(), self.printable())
  }
  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, but it may still cross lines: a long key is often
/// written that way.
fn Reader::plain(
  self : Reader,
  mode : Mode,
  indent : Int32,
) -> String raise Refused {
  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. In flow context a colon is only an
      // indicator when something ends the scalar after it; `a:b` and `:x` are
      // text (§7.3.3).
      if c == 0x3a {
        let next = self.ahead(1)
        let blank = next is (-1 | 0x20 | 0x09 | 0x0a | 0x0d)
        let indicator = mode is Flow &&
          next is (0x2c | 0x5b | 0x5d | 0x7b | 0x7d)
        if blank || indicator {
          break
        }
      }
      if mode is Flow &&
        (c == 0x2c || c == 0x5b || c == 0x5d || c == 0x7b || c == 0x7d) {
        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
    } else if first && mode is Flow {
      // Nothing plain starts with an indicator, and an empty entry is a fault.
      let c = self.cur()
      if c == 0x2c || c == 0x5d || c == 0x7d || c == 0x3a {
        raise Unexpected(self.at(), self.printable())
      }
    }
    // A key is one line; anything else may continue on the next.
    if mode is Key {
      break
    }
    let save = (self.pos, self.line, self.bol)
    let mut breaks = 0
    let mut commented = false
    for ;; {
      self.gap()
      if self.at_note() {
        // A comment ends the scalar; the line under it cannot continue it.
        commented = true
        break
      }
      if self.brk() {
        breaks = breaks + 1
        continue
      }
      break
    }
    if commented || breaks == 0 || self.eof() {
      self.pos = save.0
      self.line = save.1
      self.bol = save.2
      break
    }
    // A document marker at the start of a line ends the scalar, and one inside a
    // scalar is a fault: `...` means what it says wherever it appears.
    if self.at_marker() || self.at_end_marker() {
      self.pos = save.0
      self.line = save.1
      self.bol = save.2
      break
    }
    if mode is Block {
      if self.column() <= indent || self.opens_pair(indent) {
        self.pos = save.0
        self.line = save.1
        self.bol = save.2
        break
      }
    } else {
      // In flow context the scalar continues while the next line still reads as
      // text rather than as the collection's own punctuation.
      let c = self.cur()
      if c == 0x2c || c == 0x5d || c == 0x7d || c == 0x3a || self.at_note() {
        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 : Int32) -> 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 : Int32) -> Bool {
  c == 0x20 || c == 0x09
}

///|
/// Whether the indentation in front of the cursor holds a tab, which §5.5 does not
/// count as indentation at all: a line indented with one is a fault.
fn Reader::tabbed(self : Reader) -> Bool {
  let mut i = self.bol
  while i < self.pos {
    if self.src[i].to_int() == 0x09 {
      return true
    }
    i = i + 1
  }
  false
}

///|
/// 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_parts(
  self : Reader,
  indent : Int32,
  folded : Bool,
) -> (String, Int32) 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
  // The widest empty line seen before the content's indentation is known: §8.1.1.1
  // makes a leading empty line wider than the first content line a fault.
  let mut widest = 0
  for ;; {
    if self.eof() {
      break
    }
    if self.column() == 0 && (self.at_marker() || self.at_end_marker()) {
      // A document marker in the first column ends the scalar: at the root there is
      // no indentation to be wider than it (§9.1.1).
      break
    }
    let bol = self.pos
    let mut col = 0
    if width < 0 && self.cur() == 0x09 {
      raise BadIndent(self.at())
    }
    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
        if col > widest {
          widest = col
        }
      } else {
        lines.push("")
      }
      continue
    }
    if self.eof() {
      break
    }
    if width < 0 {
      if col <= indent {
        self.pos = bol
        break
      }
      if col < widest {
        raise BadIndent(self.at())
      }
      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 (chomped("", leading, chomp), chomp)
  }
  let out = StringBuilder()
  let mut pending = 0
  let mut more_before = false
  let mut started = false
  for i = 0; i < lines.length(); i = i + 1 {
    let line = lines[i]
    if line.length() == 0 {
      pending = pending + 1
      continue
    }
    let more = is_blank(line[0].to_int())
    if !started {
      // Leading empty lines are breaks, however many there were.
      for k = 0; k < pending; k = k + 1 {
        out.write_char('\n')
      }
      started = true
    } else if folded && pending == 0 && !more_before && !more {
      // Two ordinary lines: the break between them folds into a space (§6.5).
      out.write_char(' ')
    } else {
      // Otherwise the break stands. A blank line is itself a break, and a
      // more-indented line keeps the break that introduces it too (§8.1.3).
      let keep = if !folded {
        pending + 1
      } else if more_before || more {
        pending + 1
      } else {
        pending
      }
      for k = 0; k < keep; k = k + 1 {
        out.write_char('\n')
      }
    }
    out.write_string(line)
    more_before = more
    pending = 0
  }
  let body = out.to_string()
  let breaks = if body.length() == 0 { pending } else { pending + 1 }
  (chomped(body, breaks, chomp), chomp)
}

///|
/// A block scalar as the value it is: a string, or whatever a tag in front of it
/// says it is (§8.1).
fn Reader::block_scalar(
  self : Reader,
  indent : Int32,
  folded : Bool,
  tag : String?,
) -> Json raise Refused {
  let at = self.at()
  let (text, _) = self.block_parts(indent, folded)
  self.resolve(text, true, tag, at)
}

///|
/// 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 : Int32, chomp : Int32) -> String {
  // Clipping keeps the final line break of the *content*; a scalar with no
  // content lines has no such break to keep, so it is empty.
  let keep = match chomp {
    -1 => 0
    1 => breaks
    _ => if breaks > 0 && body.length() > 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 rules the schema carries.
///
/// 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.
///
/// This is the resolver every version in this library shares; a version differs in
/// its [`Rules`], and a dialect that differs in more than its types supplies a
/// function of its own through [`Schema::resolve`].
fn resolve_rules(
  text : String,
  quoted : Bool,
  tag : String?,
  at : At,
  rules : Rules,
) -> Json raise Refused {
  match tag {
    Some(t) => return tagged(text, t, quoted, at, rules)
    None => ()
  }
  if quoted {
    return Json::string(text)
  }
  untagged(text, rules)
}

///|
/// A scalar with no tag, resolved by what it looks like.
fn untagged(text : String, rules : Rules) -> Json {
  if rules.strings {
    // StrictYAML's rule: nothing is typed but by the schema it is read against.
    return Json::string(text)
  }
  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)
    _ => ()
  }
  if rules.yes_no {
    match text {
      "yes" | "Yes" | "YES" | "on" | "On" | "ON" => return Json::boolean(true)
      "no" | "No" | "NO" | "off" | "Off" | "OFF" => return Json::boolean(false)
      _ => ()
    }
  }
  if rules.letters {
    match text {
      "y" | "Y" => return Json::boolean(true)
      "n" | "N" => return Json::boolean(false)
      _ => ()
    }
  }
  match number(text, rules) {
    Some(n) => Json::number(n)
    None => Json::string(text)
  }
}

///|
/// A scalar under an explicit tag.
///
/// The core schema's own tags are honoured. A tag from another vocabulary — a local
/// `!foo`, a `!!python/object`, an unregistered `tag:example.com,2000:app/foo` — is
/// not resolved: this reader has no constructor for it, so the scalar is what it
/// would have been without the tag. That is what §10.4 permits a processor to do,
/// and it is the only reading that does not invent a type.
fn tagged(
  text : String,
  tag : String,
  quoted : Bool,
  at : At,
  rules : Rules,
) -> 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" =>
      // An explicit `!!bool` is a caller asking for a boolean, so every spelling
      // any version ever gave it counts — the tag removes the ambiguity the words
      // have when they are bare.
      match untagged(text, { ..rules, yes_no: true, }) {
        b if b is True || b is False => b
        _ => raise Unexpected(at, '?')
      }
    "tag:yaml.org,2002:int" | "tag:yaml.org,2002:float" =>
      match number(text, rules) {
        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, rules)
    // `!!seq` and `!!map` name collections; a scalar cannot be one.
    "tag:yaml.org,2002:seq" | "tag:yaml.org,2002:map" => raise BadTag(at, tag)
    _ => if quoted { Json::string(text) } else { untagged(text, rules) }
  }
}

///|
/// A numeric literal under the given rules, or `None` for anything that is not
/// wholly numeric so the caller falls back to a string.
///
/// YAML 1.2's core schema (§10.2.1.2, §10.2.1.3) has decimal, `0o` octal and `0x`
/// hexadecimal integers, floats with an optional exponent, and `.inf` and `.nan` in
/// either case. The rules add what the older versions had: a `0` prefix as octal,
/// `0b` as binary, underscores as digit separators, and base-60.
fn number(s : String, rules : Rules) -> Double? {
  if s.length() == 0 {
    return None
  }
  let text = if rules.underscores { strip_underscores(s) } else { s }
  let mut i = 0
  let mut sign = 1.0
  if text[0].to_int() == 0x2d {
    sign = -1.0
    i = 1
  } else if text[0].to_int() == 0x2b {
    i = 1
  }
  if i >= text.length() {
    return None
  }
  let rest = text[i:].to_owned()
  if i == 0 {
    match rest {
      ".nan" | ".NaN" | ".NAN" => return Some(@double.not_a_number)
      _ => ()
    }
  }
  match rest {
    ".inf" | ".Inf" | ".INF" => return Some(sign * @double.infinity)
    _ => ()
  }
  if rules.sexagesimal {
    match base_sixty(rest) {
      Some(n) => return Some(sign * n)
      None => ()
    }
  }
  // `0o` and `0x` are whole integers or nothing; `0b` and a bare `0` are the older
  // spellings the rules switch on. A schema that spells octal as a leading zero is
  // older than the `0o` spelling, so it does not have it: `0o14` is a string in 1.1,
  // which is what PyYAML and libyaml make of it.
  if rest.length() > 2 && rest[0].to_int() == 0x30 {
    let radix = match rest[1].to_int() {
      0x6f | 0x4f => if rules.octal { 0 } else { 8 }
      0x78 | 0x58 => 16
      0x62 | 0x42 => if rules.binary { 2 } else { 0 }
      _ => 0
    }
    if radix != 0 {
      match digits_in(rest[2:], radix) {
        Some(n) => return Some(sign * n)
        None => return None
      }
    }
    if rules.octal && rest[1].to_int() >= 0x30 && rest[1].to_int() <= 0x37 {
      match digits_in(rest[1:], 8) {
        Some(n) => return Some(sign * n)
        None => return None
      }
    }
  }
  let mut whole = 0.0
  let mut digits = false
  while i < text.length() &&
        text[i].to_int() >= 0x30 &&
        text[i].to_int() <= 0x39 {
    whole = whole * 10.0 + (text[i].to_int() - 0x30).to_double()
    digits = true
    i = i + 1
  }
  let mut value = whole
  if i < text.length() && text[i].to_int() == 0x2e {
    i = i + 1
    let mut frac = 0.0
    let mut scale = 1.0
    while i < text.length() &&
          text[i].to_int() >= 0x30 &&
          text[i].to_int() <= 0x39 {
      frac = frac * 10.0 + (text[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 < text.length() && (text[i].to_int() == 0x65 || text[i].to_int() == 0x45) {
    i = i + 1
    let mut esign = 1
    if i < text.length() &&
      (text[i].to_int() == 0x2b || text[i].to_int() == 0x2d) {
      if text[i].to_int() == 0x2d {
        esign = -1
      }
      i = i + 1
    }
    let mut exp = 0
    let mut any = false
    while i < text.length() &&
          text[i].to_int() >= 0x30 &&
          text[i].to_int() <= 0x39 {
      exp = exp * 10 + (text[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 != text.length() {
    return None
  }
  Some(sign * value)
}

///|
/// A number with its digit separators taken out.
fn strip_underscores(s : String) -> String {
  if index_of_unit(s, 0x5f) < 0 {
    return s
  }
  let out = StringBuilder()
  for i = 0; i < s.length(); i = i + 1 {
    if s[i].to_int() != 0x5f {
      out.write_char(s[i].unsafe_to_char())
    }
  }
  out.to_string()
}

///|
/// A run of digits in a radix, or `None` if there is anything else in it.
fn digits_in(s : StringView, radix : Int32) -> Double? {
  if s.length() == 0 {
    return None
  }
  let mut acc = 0.0
  for i = 0; i < s.length(); i = i + 1 {
    guard nibble(s[i].to_int()) is Some(d) else { return None }
    if d >= radix {
      return None
    }
    acc = acc * radix.to_double() + d.to_double()
  }
  Some(acc)
}

///|
/// A base-60 number of the form YAML 1.1 gives: `1:30` is ninety, `1:30:30` is five
/// thousand four hundred and thirty, and the last part may carry a fraction. The
/// parts are not restricted to two digits, which is what makes `1:234` ambiguous —
/// 1.1 resolves it as a number, and so does this.
fn base_sixty(text : String) -> Double? {
  let colon = index_of_unit(text, 0x3a)
  if colon <= 0 {
    return None
  }
  let mut total = 0.0
  let mut start = 0
  for i = 0; i <= text.length(); i = i + 1 {
    if i == text.length() || text[i].to_int() == 0x3a {
      let part = text[start:i].to_owned()
      if part.length() == 0 {
        return None
      }
      let value = if i == text.length() {
        match plain_decimal(part) {
          Some(v) => v
          None => return None
        }
      } else {
        match digits_in(part[:], 10) {
          Some(v) => v
          None => return None
        }
      }
      total = total * 60.0 + value
      start = i + 1
    }
  }
  Some(total)
}

///|
/// A decimal number, sign and fraction included, with nothing else in it.
fn plain_decimal(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
  }
  let mut value = 0.0
  let mut digits = false
  while i < s.length() && s[i].to_int() >= 0x30 && s[i].to_int() <= 0x39 {
    value = value * 10.0 + (s[i].to_int() - 0x30).to_double()
    digits = true
    i = i + 1
  }
  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 || i != s.length() {
    return None
  }
  Some(sign * value)
}