///|
priv struct Line {
  number : Int32
  indent : Int32
  has_tab_indent : Bool
  raw : String
  content : String
}

///|
fn block_indent(line : Line) -> Int32 {
  if line.has_tab_indent && line.indent > 0 {
    line.indent + 1
  } else {
    line.indent
  }
}

///|
fn block_content(line : Line) -> String {
  if line.has_tab_indent && line.indent > 0 {
    let raw = line.raw.view(start_offset=line.indent + 1).to_string()
    let stripped = strip_inline_comment(raw)
    stripped.trim_end().to_string()
  } else {
    line.content
  }
}

///|
fn drop_first_char(text : String) -> String {
  let chars = text.to_array()
  if chars.length() <= 1 {
    ""
  } else {
    text.view(start_offset=1).to_string()
  }
}

///|
fn make_virtual_line(number : Int32, indent : Int32, content : String) -> Line {
  let sb = StringBuilder::new()
  for _ in 0.. Int32 {
  let next_index = next_non_empty(lines, line_index + 1)
  if next_index < lines.length() {
    let next_line = lines[next_index]
    let next_indent = block_indent(next_line)
    if next_indent > parent_indent &&
      next_indent > default_indent &&
      is_seq_line(next_line, next_indent) {
      return next_indent
    }
  }
  default_indent
}

///|
fn inline_virtual_indent_map(
  lines : Array[Line],
  line_index : Int32,
  parent_indent : Int32,
  default_indent : Int32,
) -> Int32 {
  let next_index = next_non_empty(lines, line_index + 1)
  if next_index < lines.length() {
    let next_line = lines[next_index]
    let next_indent = block_indent(next_line)
    if next_indent > parent_indent &&
      next_indent > default_indent &&
      is_map_line(next_line, next_indent) {
      return next_indent
    }
  }
  default_indent
}

///|
priv enum Chomp {
  Clip
  Strip
  Keep
}

///|
fn parse_block_document(
  ctx : TagContext,
  input : String,
) -> Yaml raise ParseError {
  let lines = collect_lines(input)
  let start = next_non_empty(lines, 0)
  if start >= lines.length() {
    return Yaml::Null
  }
  let first = lines[start]
  let (value, next) = parse_block_node(ctx, lines, start, first.indent, true)
  let rest = next_non_empty(lines, next)
  if rest < lines.length() {
    let line = lines[rest]
    raise ParseError::UnexpectedChar(
      pos={ line: line.number, column: line.indent + 1, },
      found='-',
    )
  }
  value
}

///|
fn parse_block_node(
  ctx : TagContext,
  lines : Array[Line],
  index : Int32,
  indent : Int32,
  allow_same_indent_props : Bool,
) -> (Yaml, Int32) raise ParseError {
  let start = next_non_empty(lines, index)
  if start >= lines.length() {
    let pos = match lines.last() {
      None => { line: 1, column: 1, }
      Some(last) => { line: last.number, column: last.indent + 1, }
    }
    raise ParseError::UnexpectedEof(pos~)
  }
  let line = lines[start]
  if line.has_tab_indent && line.indent == 0 {
    raise ParseError::InvalidIndent(pos={
      line: line.number,
      column: line.indent + 1,
    })
  }
  let line_indent = block_indent(line)
  let line_content = block_content(line)
  if indicator_has_tab(line_content, '?') ||
    indicator_has_tab(line_content, ':') {
    raise ParseError::InvalidIndent(pos={
      line: line.number,
      column: line.indent + 1,
    })
  }
  if line_indent < indent {
    raise ParseError::InvalidIndent(pos={
      line: line.number,
      column: line.indent + 1,
    })
  }
  if is_seq_line(line, indent) {
    parse_block_seq(ctx, lines, start, indent)
  } else if is_map_start(lines, start, indent) {
    parse_block_map(ctx, lines, start, indent)
  } else {
    parse_value_from_text(
      ctx, lines, start, indent, line_content, allow_same_indent_props,
    )
  }
}

///|
fn next_non_empty(lines : Array[Line], index : Int32) -> Int32 {
  let mut i = index
  while i < lines.length() && lines[i].content is "" {
    i = i + 1
  }
  i
}

///|
fn parse_block_seq(
  ctx : TagContext,
  lines : Array[Line],
  index : Int32,
  indent : Int32,
) -> (Yaml, Int32) raise ParseError {
  let items : Array[Yaml] = []
  let mut i = index
  while i < lines.length() {
    i = next_non_empty(lines, i)
    if i >= lines.length() {
      break
    }
    let line = lines[i]
    let line_indent = block_indent(line)
    let line_content = block_content(line)
    if line.has_tab_indent && line.indent == 0 && !(line_content is "") {
      raise ParseError::InvalidIndent(pos={
        line: line.number,
        column: line.indent + 1,
      })
    }
    if line_indent < indent {
      break
    }
    if line_indent != indent || !is_seq_line(line, indent) {
      break
    }
    let (sep_has_tab, rest) = split_indicator_rest(line_content, '-')
    if sep_has_tab && starts_with_block_indicator(rest) {
      raise ParseError::InvalidIndent(pos={
        line: line.number,
        column: line.indent + 1,
      })
    }
    if rest is "" {
      let next_index = next_non_empty(lines, i + 1)
      if next_index >= lines.length() ||
        block_indent(lines[next_index]) <= indent {
        items.push(Yaml::Null)
        i = next_index
      } else {
        let child_indent = block_indent(lines[next_index])
        let (value, next) = parse_block_node(
          ctx, lines, next_index, child_indent, true,
        )
        items.push(value)
        i = next
      }
    } else {
      match try_parse_block_scalar(ctx, rest, lines, i + 1, indent) {
        Some((node, next)) => {
          items.push(node)
          i = next
        }
        None =>
          match parse_props_only(ctx, rest) {
            Some((tag, anchor)) => {
              let next_index = next_non_empty(lines, i + 1)
              if next_index < lines.length() &&
                block_indent(lines[next_index]) > indent {
                let child_indent = block_indent(lines[next_index])
                let (value, next) = parse_block_node(
                  ctx, lines, next_index, child_indent, true,
                )
                items.push(
                  apply_node_props(value, tag, anchor, ctx.coerce_binary),
                )
                i = next
              } else {
                items.push(
                  apply_node_props(Yaml::Null, tag, anchor, ctx.coerce_binary),
                )
                i = i + 1
              }
            }
            None =>
              if rest is "-" || rest.has_prefix("- ") {
                let virtual_indent = inline_virtual_indent_seq(
                  lines,
                  i,
                  indent,
                  indent + 2,
                )
                let virtual_line = make_virtual_line(
                  line.number,
                  virtual_indent,
                  rest,
                )
                let temp : Array[Line] = [virtual_line]
                for j in (i + 1).. (Yaml, Int32) raise ParseError {
  let items : Array[(Yaml, Yaml)] = []
  let mut i = index
  while i < lines.length() {
    i = next_non_empty(lines, i)
    if i >= lines.length() {
      break
    }
    let line = lines[i]
    let line_indent = block_indent(line)
    let line_content = block_content(line)
    if line.has_tab_indent && line.indent == 0 {
      raise ParseError::InvalidIndent(pos={
        line: line.number,
        column: line.indent + 1,
      })
    }
    if indicator_has_tab(line_content, '?') ||
      indicator_has_tab(line_content, ':') {
      raise ParseError::InvalidIndent(pos={
        line: line.number,
        column: line.indent + 1,
      })
    }
    if line_indent < indent {
      break
    }
    if line_indent != indent {
      break
    }
    if is_explicit_key_line(line, indent) {
      let (entry, next) = parse_explicit_entry(ctx, lines, i, indent)
      items.push(entry)
      i = next
    } else if split_key_value_opt(line_content) is Some(_) {
      let (key_text, value_text) = split_key_value(line)
      let key = parse_inline_key(ctx, key_text)
      let trimmed_value = value_text.trim_start().to_string()
      if trimmed_value.has_prefix("- ") {
        raise ParseError::InvalidIndent(pos={
          line: line.number,
          column: line.indent + 1,
        })
      }
      if is_plain_scalar_start(trimmed_value) &&
        has_plain_key_indicator(trimmed_value) {
        raise ParseError::UnexpectedChar(
          pos={ line: line.number, column: line.indent + 1, },
          found=':',
        )
      }
      let (value, next) = parse_value_from_text(
        ctx, lines, i, indent, value_text, false,
      )
      items.push((key, value))
      i = next
    } else if is_value_line_after(lines, i, indent) {
      let key = parse_inline_key(ctx, line_content)
      let value_line = next_non_empty(lines, i + 1)
      let value_text = drop_first_char(block_content(lines[value_line]))
      let (value, next) = parse_value_from_text(
        ctx, lines, value_line, indent, value_text, false,
      )
      items.push((key, value))
      i = next
    } else if !is_map_line(line, indent) {
      break
    }
  }
  (Yaml::Map(items), i)
}

///|
fn parse_inline_value(ctx : TagContext, text : String) -> Yaml raise ParseError {
  let trimmed = text.trim().to_string()
  if trimmed is "" {
    return Yaml::Null
  }
  let scanner = Scanner::new(trimmed)
  scanner.skip_ws_and_comments()
  match parse_alias(scanner) {
    Some(name) => Yaml::Alias(name)
    None => {
      let (tag, anchor) = parse_node_props(ctx, scanner)
      scanner.skip_ws_and_comments()
      if (tag is Some(_) || anchor is Some(_)) && scanner.peek() is Some('*') {
        raise ParseError::InvalidTagValue(tag="alias", value="*")
      }
      let rest_pos = scanner.position()
      let rest = trimmed
        .view(start_offset=scanner.offset())
        .to_string()
        .trim_start()
        .to_string()
      let has_props = tag is Some(_) || anchor is Some(_)
      let value = if rest is "" {
        Yaml::Null
      } else if rest.has_prefix("[") || rest.has_prefix("{") {
        parse_flow_value_from_string(ctx, rest)
      } else if rest.has_prefix("\"") || rest.has_prefix("'") {
        parse_flow_value_from_string(ctx, rest)
      } else {
        if has_props && (rest.has_prefix("- ") || rest.has_prefix("-\t")) {
          raise ParseError::UnexpectedChar(pos=rest_pos, found='-')
        }
        if has_props && rest.has_prefix(",") {
          raise ParseError::UnexpectedChar(pos=rest_pos, found=',')
        }
        parse_scalar_value(rest)
      }
      apply_node_props(value, tag, anchor, ctx.coerce_binary)
    }
  }
}

///|
fn parse_inline_node_multiline(
  ctx : TagContext,
  lines : Array[Line],
  line_index : Int32,
  indent : Int32,
  text : String,
  allow_same_indent_props : Bool,
) -> (Yaml, Int32) raise ParseError {
  let trimmed = text.trim().to_string()
  let mut inline_comment = false
  if line_index < lines.length() {
    let raw_line = lines[line_index].raw
      .view(start_offset=lines[line_index].indent)
      .to_string()
    let stripped = strip_inline_comment(raw_line)
    if stripped.length() < raw_line.length() {
      inline_comment = true
    }
  }
  if trimmed is "" {
    return (Yaml::Null, line_index + 1)
  }
  let scanner = Scanner::new(trimmed)
  scanner.skip_ws_and_comments()
  match parse_alias(scanner) {
    Some(name) => (Yaml::Alias(name), line_index + 1)
    None => {
      let (tag, anchor) = parse_node_props(ctx, scanner)
      scanner.skip_ws_and_comments()
      if (tag is Some(_) || anchor is Some(_)) && scanner.peek() is Some('*') {
        raise ParseError::InvalidTagValue(tag="alias", value="*")
      }
      let rest_pos = scanner.position()
      let rest = trimmed
        .view(start_offset=scanner.offset())
        .to_string()
        .trim_start()
        .to_string()
      let has_props = tag is Some(_) || anchor is Some(_)
      if rest is "" {
        let value = Yaml::Null
        (
          apply_node_props(value, tag, anchor, ctx.coerce_binary),
          line_index + 1,
        )
      } else if rest.has_prefix("[") || rest.has_prefix("{") {
        let value = parse_flow_value_from_string(ctx, rest)
        (
          apply_node_props(value, tag, anchor, ctx.coerce_binary),
          line_index + 1,
        )
      } else if rest.has_prefix("\"") || rest.has_prefix("'") {
        let value = parse_flow_value_from_string(ctx, rest)
        (
          apply_node_props(value, tag, anchor, ctx.coerce_binary),
          line_index + 1,
        )
      } else if is_plain_scalar_start(rest) {
        if has_props && (rest.has_prefix("- ") || rest.has_prefix("-\t")) {
          raise ParseError::UnexpectedChar(pos=rest_pos, found='-')
        }
        if has_props && rest.has_prefix(",") {
          raise ParseError::UnexpectedChar(pos=rest_pos, found=',')
        }
        if inline_comment {
          let value = parse_scalar_value(rest)
          let node = apply_node_props(value, tag, anchor, ctx.coerce_binary)
          (node, line_index + 1)
        } else {
          let (parts, next) = collect_plain_continuation(
            lines,
            line_index + 1,
            indent,
            allow_same_indent_props,
          )
          let joined = if parts.length() > 0 {
            fold_plain_lines(rest, parts)
          } else {
            rest
          }
          let value = parse_scalar_value(joined)
          let node = apply_node_props(value, tag, anchor, ctx.coerce_binary)
          if parts.length() > 0 {
            (node, next)
          } else {
            (node, line_index + 1)
          }
        }
      } else {
        let value = parse_scalar_value(rest)
        (
          apply_node_props(value, tag, anchor, ctx.coerce_binary),
          line_index + 1,
        )
      }
    }
  }
}

///|
fn parse_inline_key(ctx : TagContext, text : String) -> Yaml raise ParseError {
  let trimmed = text.trim().to_string()
  if trimmed is "" {
    Yaml::Null
  } else {
    parse_inline_value(ctx, text)
  }
}

///|
fn parse_value_from_text(
  ctx : TagContext,
  lines : Array[Line],
  line_index : Int32,
  indent : Int32,
  value_text : String,
  allow_same_indent_props : Bool,
) -> (Yaml, Int32) raise ParseError {
  let raw_value = if value_text == block_content(lines[line_index]) {
    lines[line_index].raw
    .view(start_offset=lines[line_index].indent)
    .to_string()
  } else {
    value_text
  }
  let base_indent = if value_text == block_content(lines[line_index]) {
    indent - 1
  } else {
    indent
  }
  let trimmed_value = value_text.trim_start().to_string()
  let raw_trimmed = raw_value.trim_start().to_string()
  if trimmed_value is "" {
    let next_index = next_non_empty(lines, line_index + 1)
    if next_index >= lines.length() {
      (Yaml::Null, next_index)
    } else if block_indent(lines[next_index]) > indent {
      let next_line = lines[next_index]
      match parse_props_only(ctx, block_content(next_line)) {
        Some((tag, anchor)) => {
          let after_props = next_non_empty(lines, next_index + 1)
          if after_props < lines.length() {
            let after_line = lines[after_props]
            let outdented_seq = block_indent(after_line) == indent &&
              is_seq_line(after_line, indent)
            if block_indent(after_line) < block_indent(next_line) &&
              (block_indent(after_line) > indent || outdented_seq) {
              match
                try_parse_block_scalar(
                  ctx,
                  block_content(after_line),
                  lines,
                  after_props + 1,
                  indent,
                ) {
                Some((value, next)) =>
                  return (
                    apply_node_props(value, tag, anchor, ctx.coerce_binary),
                    next,
                  )
                None => {
                  let (value, next) = parse_block_node(
                    ctx,
                    lines,
                    after_props,
                    block_indent(after_line),
                    true,
                  )
                  return (
                    apply_node_props(value, tag, anchor, ctx.coerce_binary),
                    next,
                  )
                }
              }
            }
          }
        }
        None => ()
      }
      let child_indent = block_indent(next_line)
      parse_block_node(ctx, lines, next_index, child_indent, true)
    } else if is_seq_line(lines[next_index], indent) {
      parse_block_seq(ctx, lines, next_index, indent)
    } else {
      (Yaml::Null, next_index)
    }
  } else {
    if trimmed_value is "-" ||
      trimmed_value.has_prefix("- ") ||
      trimmed_value.has_prefix("-\t") {
      let virtual_indent = inline_virtual_indent_seq(
        lines,
        line_index,
        indent,
        indent + 2,
      )
      let virtual_line = make_virtual_line(
        lines[line_index].number,
        virtual_indent,
        trimmed_value,
      )
      let temp : Array[Line] = [virtual_line]
      for i in (line_index + 1).. return (node, next)
      None => ()
    }
    match try_parse_flow_multiline(ctx, trimmed_value, lines, line_index) {
      Some((node, next)) => return (node, next)
      None => ()
    }
    match
      try_parse_props_only(
        ctx, trimmed_value, lines, line_index, indent, allow_same_indent_props,
      ) {
      Some((node, next)) => return (node, next)
      None => ()
    }
    match
      try_parse_block_scalar(
        ctx,
        trimmed_value,
        lines,
        line_index + 1,
        base_indent,
      ) {
      Some((node, next)) => (node, next)
      None =>
        parse_inline_node_multiline(
          ctx, lines, line_index, indent, trimmed_value, allow_same_indent_props,
        )
    }
  }
}

///|
fn try_parse_props_only(
  ctx : TagContext,
  text : String,
  lines : Array[Line],
  line_index : Int32,
  indent : Int32,
  allow_same_indent_props : Bool,
) -> (Yaml, Int32)? raise ParseError {
  let scanner = Scanner::new(text)
  scanner.skip_ws_and_comments()
  if parse_alias(scanner) is Some(_) {
    return None
  }
  let (tag, anchor) = parse_node_props(ctx, scanner)
  scanner.skip_ws_and_comments()
  let rest = text
    .view(start_offset=scanner.offset())
    .to_string()
    .trim_start()
    .to_string()
  if rest is "" && (tag is Some(_) || anchor is Some(_)) {
    let next_index = next_non_empty(lines, line_index + 1)
    if next_index < lines.length() {
      let next_line = lines[next_index]
      if block_indent(next_line) > indent {
        let child_indent = block_indent(next_line)
        let (value, next) = parse_block_node(
          ctx, lines, next_index, child_indent, true,
        )
        return Some(
          (apply_node_props(value, tag, anchor, ctx.coerce_binary), next),
        )
      }
      if block_indent(next_line) == indent && is_seq_line(next_line, indent) {
        let (value, next) = parse_block_seq(ctx, lines, next_index, indent)
        return Some(
          (apply_node_props(value, tag, anchor, ctx.coerce_binary), next),
        )
      }
      if allow_same_indent_props && block_indent(next_line) == indent {
        let (value, next) = parse_block_node(
          ctx, lines, next_index, indent, true,
        )
        return Some(
          (apply_node_props(value, tag, anchor, ctx.coerce_binary), next),
        )
      }
    }
    return Some(
      (
        apply_node_props(Yaml::Null, tag, anchor, ctx.coerce_binary),
        line_index + 1,
      ),
    )
  }
  None
}

///|
fn parse_props_only(
  ctx : TagContext,
  text : String,
) -> (String?, String?)? raise ParseError {
  let scanner = Scanner::new(text)
  scanner.skip_ws_and_comments()
  if parse_alias(scanner) is Some(_) {
    return None
  }
  let (tag, anchor) = parse_node_props(ctx, scanner)
  scanner.skip_ws_and_comments()
  let rest = text
    .view(start_offset=scanner.offset())
    .to_string()
    .trim()
    .to_string()
  if rest is "" && (tag is Some(_) || anchor is Some(_)) {
    Some((tag, anchor))
  } else {
    None
  }
}

///|
fn try_parse_quoted_props(
  ctx : TagContext,
  text : String,
  lines : Array[Line],
  line_index : Int32,
) -> (Yaml, Int32)? raise ParseError {
  let scanner = Scanner::new(text)
  scanner.skip_ws_and_comments()
  if parse_alias(scanner) is Some(_) {
    return None
  }
  let (tag, anchor) = parse_node_props(ctx, scanner)
  scanner.skip_ws_and_comments()
  let rest = text
    .view(start_offset=scanner.offset())
    .to_string()
    .trim_start()
    .to_string()
  if rest.has_prefix("\"") && !is_terminated_double(rest) {
    let (combined, next) = collect_quoted_multiline(
      lines,
      line_index,
      '"',
      rest,
      lines[line_index].indent,
    )
    let value = parse_flow_value_from_string(ctx, combined)
    return Some((apply_node_props(value, tag, anchor, ctx.coerce_binary), next))
  }
  if rest.has_prefix("'") && !is_terminated_single(rest) {
    let (combined, next) = collect_quoted_multiline(
      lines,
      line_index,
      '\'',
      rest,
      lines[line_index].indent,
    )
    let value = parse_flow_value_from_string(ctx, combined)
    return Some((apply_node_props(value, tag, anchor, ctx.coerce_binary), next))
  }
  None
}

///|
fn try_parse_flow_multiline(
  ctx : TagContext,
  text : String,
  lines : Array[Line],
  line_index : Int32,
) -> (Yaml, Int32)? raise ParseError {
  let scanner = Scanner::new(text)
  scanner.skip_ws_and_comments()
  if parse_alias(scanner) is Some(_) {
    return None
  }
  let (tag, anchor) = parse_node_props(ctx, scanner)
  scanner.skip_ws_and_comments()
  let rest = text
    .view(start_offset=scanner.offset())
    .to_string()
    .trim_start()
    .to_string()
  if rest.has_prefix("[") || rest.has_prefix("{") {
    let base_indent = lines[line_index].indent
    let (combined, next) = collect_flow_multiline(
      lines, line_index, rest, base_indent,
    )
    let value = parse_flow_value_from_string(ctx, combined)
    return Some((apply_node_props(value, tag, anchor, ctx.coerce_binary), next))
  }
  None
}

///|
fn collect_flow_multiline(
  lines : Array[Line],
  index : Int32,
  first : String,
  base_indent : Int32,
) -> (String, Int32) raise ParseError {
  let sb = StringBuilder::new()
  sb.write_string(first)
  let mut depth = 0
  let mut in_single = false
  let mut in_double = false
  let (next_depth, next_single, next_double) = scan_flow_depth(
    first, depth, in_single, in_double,
  )
  depth = next_depth
  in_single = next_single
  in_double = next_double
  let mut i = index + 1
  while depth > 0 && i < lines.length() {
    let line = lines[i]
    if line.has_tab_indent &&
      !(line.content is "") &&
      line.indent <= base_indent {
      raise ParseError::InvalidIndent(pos={
        line: line.number,
        column: line.indent + 1,
      })
    }
    if !(line.content is "") && line.indent <= base_indent {
      let trimmed = line.content.trim().to_string()
      if !(trimmed is "]" || trimmed is "}") {
        raise ParseError::InvalidIndent(pos={
          line: line.number,
          column: line.indent + 1,
        })
      }
    }
    sb.write_char('\n')
    let line_text = line.raw.view(start_offset=line.indent).to_string()
    sb.write_string(line_text)
    let (next_depth, next_single, next_double) = scan_flow_depth(
      line_text, depth, in_single, in_double,
    )
    depth = next_depth
    in_single = next_single
    in_double = next_double
    i = i + 1
  }
  (sb.to_string(), i)
}

///|
fn scan_flow_depth(
  text : String,
  depth : Int32,
  in_single : Bool,
  in_double : Bool,
) -> (Int32, Bool, Bool) {
  let mut d = depth
  let mut single = in_single
  let mut double = in_double
  let chars = text.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if double {
      if c is '\\' && i + 1 < chars.length() {
        i = i + 2
        continue
      }
      if c is '"' {
        double = false
      }
      i = i + 1
      continue
    }
    if single {
      if c is '\'' {
        single = false
      }
      i = i + 1
      continue
    }
    if c is '"' {
      double = true
      i = i + 1
      continue
    }
    if c is '\'' {
      single = true
      i = i + 1
      continue
    }
    if c is '[' || c is '{' {
      d = d + 1
    } else if c is ']' || c is '}' {
      if d > 0 {
        d = d - 1
      }
    }
    i = i + 1
  }
  (d, single, double)
}

///|
fn collect_quoted_multiline(
  lines : Array[Line],
  index : Int32,
  quote : Char,
  first : String,
  base_indent : Int32,
) -> (String, Int32) raise ParseError {
  let mut content_indent : Int32? = None
  let mut combined = first
  let mut i = index + 1
  while i < lines.length() {
    let line = lines[i]
    let raw = line.raw.view(start_offset=line.indent).to_string()
    if raw.trim() is "" {
      combined = combined + "\n"
      i = i + 1
      continue
    }
    let candidate = combined + "\n" + raw
    let terminates = if quote is '"' {
      is_terminated_double(candidate)
    } else {
      is_terminated_single(candidate)
    }
    if content_indent is None {
      if (line.has_tab_indent && line.indent == 0) || line.indent <= base_indent {
        raise ParseError::InvalidIndent(pos={
          line: line.number,
          column: line.indent + 1,
        })
      }
      content_indent = Some(line.indent)
    } else {
      let effective_indent = if line.has_tab_indent {
        line.indent + 1
      } else {
        line.indent
      }
      if effective_indent < content_indent.unwrap_or(effective_indent) {
        if terminates {
          return (candidate, i + 1)
        }
        raise ParseError::InvalidIndent(pos={
          line: line.number,
          column: line.indent + 1,
        })
      }
    }
    if line.indent == 0 &&
      content_indent.unwrap_or(0) == 0 &&
      is_doc_marker_line(raw) {
      raise ParseError::InvalidIndent(pos={ line: line.number, column: 1, })
    }
    combined = candidate
    if terminates {
      return (combined, i + 1)
    }
    i = i + 1
  }
  raise ParseError::UnexpectedEof(pos={
    line: match lines.last() {
      None => 1
      Some(last) => last.number
    },
    column: 1,
  })
}

///|
fn is_doc_marker_line(text : String) -> Bool {
  if text.has_prefix("---") || text.has_prefix("...") {
    let chars = text.to_array()
    if chars.length() == 3 {
      return true
    }
    chars[3].is_whitespace()
  } else {
    false
  }
}

///|
fn is_terminated_double(text : String) -> Bool {
  let chars = text.to_array()
  let mut escaped = false
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if i == 0 && c is '"' {
      i = i + 1
      continue
    }
    if escaped {
      escaped = false
      i = i + 1
      continue
    }
    if c is '\\' {
      escaped = true
      i = i + 1
      continue
    }
    if c is '"' {
      return true
    }
    i = i + 1
  }
  false
}

///|
fn is_terminated_single(text : String) -> Bool {
  let chars = text.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if i == 0 && c is '\'' {
      i = i + 1
      continue
    }
    if c is '\'' {
      if i + 1 < chars.length() && chars[i + 1] is '\'' {
        i = i + 2
        continue
      } else {
        return true
      }
    }
    i = i + 1
  }
  false
}

///|
fn parse_explicit_entry(
  ctx : TagContext,
  lines : Array[Line],
  index : Int32,
  indent : Int32,
) -> ((Yaml, Yaml), Int32) raise ParseError {
  let line = lines[index]
  let rest = drop_first_char(block_content(line)).trim_start().to_string()
  if rest is "" {
    let next_index = next_non_empty(lines, index + 1)
    if next_index >= lines.length() || lines[next_index].indent < indent {
      raise ParseError::InvalidIndent(pos={
        line: line.number,
        column: line.indent + 1,
      })
    }
    let (key, next_after_key) = parse_block_node(
      ctx,
      lines,
      next_index,
      lines[next_index].indent,
      false,
    )
    let value_line = next_non_empty(lines, next_after_key)
    if value_line < lines.length() && is_value_line(lines[value_line], indent) {
      let value_text = drop_first_char(block_content(lines[value_line]))
      let (value, next) = parse_value_from_text(
        ctx, lines, value_line, indent, value_text, false,
      )
      ((key, value), next)
    } else {
      ((key, Yaml::Null), next_after_key)
    }
  } else {
    match try_parse_block_scalar(ctx, rest, lines, index + 1, indent) {
      Some((key, next_after_key)) => {
        let value_line = next_non_empty(lines, next_after_key)
        if value_line < lines.length() &&
          is_value_line(lines[value_line], indent) {
          let value_text = drop_first_char(block_content(lines[value_line]))
          let (value, next) = parse_value_from_text(
            ctx, lines, value_line, indent, value_text, false,
          )
          ((key, value), next)
        } else {
          ((key, Yaml::Null), next_after_key)
        }
      }
      None =>
        match split_key_value_opt(rest) {
          Some(_) => {
            let virtual_indent = inline_virtual_indent_map(
              lines,
              index,
              indent,
              indent + 2,
            )
            let virtual_line = make_virtual_line(
              line.number,
              virtual_indent,
              rest,
            )
            let temp : Array[Line] = [virtual_line]
            for i in (index + 1).. {
            let (key, next_after_key) = parse_value_from_text(
              ctx, lines, index, indent, rest, false,
            )
            let value_line = next_non_empty(lines, next_after_key)
            if value_line < lines.length() &&
              is_value_line(lines[value_line], indent) {
              let value_text = drop_first_char(block_content(lines[value_line]))
              let (value, next) = parse_value_from_text(
                ctx, lines, value_line, indent, value_text, false,
              )
              ((key, value), next)
            } else {
              ((key, Yaml::Null), next_after_key)
            }
          }
        }
    }
  }
}

///|
fn is_plain_scalar_start(text : String) -> Bool {
  if text is "" {
    return false
  }
  let trimmed = text.trim_start().to_string()
  !(trimmed.has_prefix("\"") ||
  trimmed.has_prefix("'") ||
  trimmed.has_prefix("[") ||
  trimmed.has_prefix("{") ||
  trimmed.has_prefix("!") ||
  trimmed.has_prefix("&") ||
  trimmed.has_prefix("*"))
}

///|
fn collect_plain_continuation(
  lines : Array[Line],
  index : Int32,
  indent : Int32,
  allow_same_indent : Bool,
) -> (Array[String], Int32) raise ParseError {
  let parts : Array[String] = []
  let mut i = index
  let mut base_indent : Int32? = None
  while i < lines.length() {
    let line = lines[i]
    let raw = line.raw.view(start_offset=line.indent).to_string()
    if raw.trim() is "" {
      let next_index = next_non_empty(lines, i + 1)
      if next_index >= lines.length() {
        break
      }
      if allow_same_indent {
        if lines[next_index].indent < indent {
          break
        }
      } else if lines[next_index].indent <= indent {
        break
      }
      parts.push("")
      i = i + 1
      continue
    }
    let effective_indent = if line.has_tab_indent {
      indent + 1
    } else {
      line.indent
    }
    if allow_same_indent {
      if effective_indent < indent {
        break
      }
    } else if effective_indent <= indent {
      break
    }
    if base_indent is None {
      base_indent = Some(effective_indent)
    }
    let base = base_indent.unwrap_or(effective_indent)
    if effective_indent < base {
      break
    }
    let slice_start = if line.has_tab_indent {
      line.indent + 1
    } else {
      line.indent
    }
    let text = line.raw.view(start_offset=slice_start).to_string()
    if has_plain_mapping_break(text) {
      raise ParseError::InvalidIndent(pos={
        line: line.number,
        column: line.indent + 1,
      })
    }
    let stripped = strip_inline_comment(text)
    let trimmed = stripped.trim_end().to_string()
    parts.push(trimmed)
    if stripped.length() < text.length() {
      i = i + 1
      break
    }
    i = i + 1
  }
  (parts, i)
}

///|
fn has_plain_mapping_break(text : String) -> Bool {
  let chars = text.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c is ':' {
      if i + 1 == chars.length() || chars[i + 1].is_whitespace() {
        return true
      }
    }
    i = i + 1
  }
  false
}

///|
fn fold_plain_lines(first : String, rest : Array[String]) -> String {
  let all : Array[String] = [first]
  for line in rest {
    all.push(line)
  }
  fold_lines(all)
}

///|
fn parse_block_indicator(
  text : String,
  pos : Position,
) -> (Char, Chomp, Int32?)? raise ParseError {
  let trimmed = text.trim().to_string()
  if trimmed is "" {
    return None
  }
  let chars = trimmed.to_array()
  let style = chars[0]
  if !(style is '|' || style is '>') {
    return None
  }
  let mut chomp = Chomp::Clip
  let mut indent : Int32? = None
  let mut chomp_set = false
  let mut indent_set = false
  let mut i = 1
  while i < chars.length() {
    let c = chars[i]
    if c is '+' {
      if chomp_set {
        raise ParseError::InvalidIndent(pos~)
      }
      chomp = Chomp::Keep
      chomp_set = true
    } else if c is '-' {
      if chomp_set {
        raise ParseError::InvalidIndent(pos~)
      }
      chomp = Chomp::Strip
      chomp_set = true
    } else if c.is_ascii_digit() {
      if indent_set {
        raise ParseError::InvalidIndent(pos~)
      }
      let digit = c.to_int() - '0'.to_int()
      if digit == 0 {
        raise ParseError::InvalidIndent(pos~)
      }
      indent = Some(digit)
      indent_set = true
    } else {
      break
    }
    i = i + 1
  }
  let mut rest = i
  while rest < chars.length() && chars[rest].is_whitespace() {
    rest = rest + 1
  }
  if rest < chars.length() {
    if chars[rest] is '#' {
      if rest == i {
        raise ParseError::InvalidIndent(pos~)
      }
    } else {
      raise ParseError::InvalidIndent(pos~)
    }
  }
  Some((style, chomp, indent))
}

///|
fn try_parse_block_scalar(
  ctx : TagContext,
  rest : String,
  lines : Array[Line],
  index : Int32,
  parent_indent : Int32,
) -> (Yaml, Int32)? raise ParseError {
  let scanner = Scanner::new(rest)
  scanner.skip_ws_and_comments()
  if parse_alias(scanner) is Some(_) {
    return None
  }
  let (tag, anchor) = parse_node_props(ctx, scanner)
  scanner.skip_ws_and_comments()
  let tail = rest
    .view(start_offset=scanner.offset())
    .to_string()
    .trim_start()
    .to_string()
  let pos_line = if lines.length() == 0 {
    None
  } else if index > 0 && index - 1 < lines.length() {
    Some(lines[index - 1])
  } else {
    lines.last()
  }
  let pos = match pos_line {
    None => { line: 1, column: 1, }
    Some(line) => { line: line.number, column: line.indent + 1, }
  }
  match parse_block_indicator(tail, pos) {
    None => None
    Some((style, chomp, indent_hint)) => {
      let (value, next) = parse_block_scalar(
        lines, index, parent_indent, style, chomp, indent_hint,
      )
      Some(
        (
          apply_node_props(Yaml::String(value), tag, anchor, ctx.coerce_binary),
          next,
        ),
      )
    }
  }
}

///|
fn parse_block_scalar(
  lines : Array[Line],
  index : Int32,
  parent_indent : Int32,
  style : Char,
  chomp : Chomp,
  indent_hint : Int32?,
) -> (String, Int32) raise ParseError {
  let mut i = index
  let mut content_indent : Int32? = None
  let collected : Array[String] = []
  let mut pending_blank = 0
  let mut max_blank_indent : Int32? = None
  while i < lines.length() {
    let line = lines[i]
    let tab_indent_threshold = match content_indent {
      Some(indent) => indent
      None =>
        match indent_hint {
          Some(hint) => parent_indent + hint
          None => parent_indent + 1
        }
    }
    if line.has_tab_indent && line.indent < tab_indent_threshold {
      raise ParseError::InvalidIndent(pos={
        line: line.number,
        column: line.indent + 1,
      })
    }
    if i == lines.length() - 1 && line.raw is "" {
      break
    }
    let raw_content = line.raw.view(start_offset=line.indent).to_string()
    let mut treat_as_blank = is_space_only(raw_content)
    match content_indent {
      Some(indent) => if line.indent > indent { treat_as_blank = false }
      None => ()
    }
    if treat_as_blank {
      let blank_threshold = match content_indent {
        Some(indent) => indent
        None => parent_indent
      }
      if line.indent < blank_threshold {
        let next_index = next_non_empty(lines, i + 1)
        if next_index >= lines.length() ||
          lines[next_index].indent < blank_threshold {
          if chomp is Chomp::Keep && content_indent is Some(_) {
            pending_blank = pending_blank + 1
            i = i + 1
            continue
          }
          break
        }
      }
      if content_indent is None && line.indent > parent_indent {
        match max_blank_indent {
          None => max_blank_indent = Some(line.indent)
          Some(prev) =>
            if line.indent > prev {
              max_blank_indent = Some(line.indent)
            }
        }
      }
      pending_blank = pending_blank + 1
      i = i + 1
      continue
    }
    if content_indent is None && line.indent <= parent_indent {
      pending_blank = 0
      break
    }
    if content_indent is None {
      match max_blank_indent {
        Some(max_indent) =>
          if line.indent < max_indent {
            raise ParseError::InvalidIndent(pos={
              line: line.number,
              column: line.indent + 1,
            })
          }
        None => ()
      }
      content_indent = match indent_hint {
        None => Some(line.indent)
        Some(hint) => Some(parent_indent + hint)
      }
    }
    let indent = content_indent.unwrap_or(line.indent)
    if line.indent < indent {
      break
    }
    for _ in 0.. 0 {
    for _ in 0.. 0 {
    base + "\n"
  } else if chomp is Chomp::Keep && index < lines.length() {
    "\n"
  } else {
    ""
  }
  let value = apply_chomp(with_newline, chomp)
  (value, i)
}

///|
fn is_space_only(text : String) -> Bool {
  let chars = text.to_array()
  for c in chars {
    if !(c is ' ') {
      return false
    }
  }
  true
}

///|
fn join_lines(lines : Array[String]) -> String {
  let sb = StringBuilder::new()
  for idx in 0.. 0 {
      sb.write_char('\n')
    }
    sb.write_string(lines[idx])
  }
  sb.to_string()
}

///|
fn fold_lines(lines : Array[String]) -> String {
  let sb = StringBuilder::new()
  let mut prev_blank = false
  for idx in 0.. 0 {
      if is_blank {
        sb.write_char('\n')
      } else if !prev_blank {
        sb.write_char(' ')
      }
    }
    sb.write_string(line)
    prev_blank = is_blank
  }
  sb.to_string()
}

///|
fn fold_block_lines(lines : Array[String]) -> String {
  let sb = StringBuilder::new()
  for idx in 0.. String {
  match chomp {
    Chomp::Clip => {
      let trimmed = trim_trailing_newlines(text)
      if trimmed is "" {
        ""
      } else {
        trimmed + "\n"
      }
    }
    Chomp::Strip => trim_trailing_newlines(text)
    Chomp::Keep => text
  }
}

///|
fn trim_trailing_newlines(text : String) -> String {
  let chars = text.to_array()
  let mut end = chars.length()
  while end > 0 && chars[end - 1] is '\n' {
    end = end - 1
  }
  if end == chars.length() {
    return text
  }
  let sb = StringBuilder::new()
  for i in 0.. Bool {
  let line_indent = block_indent(line)
  if line_indent != indent {
    return false
  }
  let content = block_content(line)
  if content is "-" {
    return true
  }
  let chars = content.to_array()
  chars.length() > 1 && chars[0] is '-' && chars[1].is_whitespace()
}

///|
fn is_map_line(line : Line, indent : Int32) -> Bool {
  let line_indent = block_indent(line)
  if line_indent != indent {
    return false
  }
  is_explicit_key_line(line, indent) ||
  split_key_value_opt(block_content(line)) is Some(_)
}

///|
fn is_map_start(lines : Array[Line], index : Int32, indent : Int32) -> Bool {
  let line = lines[index]
  let line_indent = block_indent(line)
  if line_indent != indent {
    return false
  }
  is_explicit_key_line(line, indent) ||
  split_key_value_opt(block_content(line)) is Some(_) ||
  is_value_line_after(lines, index, indent)
}

///|
fn is_explicit_key_line(line : Line, indent : Int32) -> Bool {
  let line_indent = block_indent(line)
  if line_indent != indent {
    return false
  }
  let content = block_content(line)
  content is "?" || content.has_prefix("? ")
}

///|
fn is_value_line(line : Line, indent : Int32) -> Bool {
  let line_indent = block_indent(line)
  if line_indent != indent {
    return false
  }
  let content = block_content(line)
  content is ":" || content.has_prefix(": ")
}

///|
fn is_value_line_after(
  lines : Array[Line],
  index : Int32,
  indent : Int32,
) -> Bool {
  let next_index = next_non_empty(lines, index + 1)
  if next_index >= lines.length() {
    return false
  }
  is_value_line(lines[next_index], indent)
}

///|
fn split_indicator_rest(content : String, indicator : Char) -> (Bool, String) {
  let chars = content.to_array()
  if chars.length() == 0 || chars[0] != indicator {
    return (false, content)
  }
  let mut i = 1
  let mut has_tab = false
  while i < chars.length() && chars[i].is_whitespace() {
    if chars[i] is '\t' {
      has_tab = true
    }
    i = i + 1
  }
  let sb = StringBuilder::new()
  for j in i.. Bool {
  let chars = text.to_array()
  if chars.length() == 0 || chars[0] != indicator {
    return false
  }
  if chars.length() == 1 {
    return true
  }
  chars[1].is_whitespace()
}

///|
fn starts_with_block_indicator(text : String) -> Bool {
  starts_with_indicator(text, '-') ||
  starts_with_indicator(text, '?') ||
  starts_with_indicator(text, ':')
}

///|
fn has_plain_key_indicator(text : String) -> Bool {
  let chars = text.to_array()
  let mut i = 0
  while i < chars.length() {
    let c = chars[i]
    if c is ':' {
      if i + 1 == chars.length() || chars[i + 1].is_whitespace() {
        return true
      }
    }
    i = i + 1
  }
  false
}

///|
fn indicator_has_tab(content : String, indicator : Char) -> Bool {
  if !starts_with_indicator(content, indicator) {
    return false
  }
  let (has_tab, _) = split_indicator_rest(content, indicator)
  has_tab
}

///|
fn split_key_value(line : Line) -> (String, String) raise ParseError {
  let content = block_content(line)
  match split_key_value_opt(content) {
    None =>
      raise ParseError::UnexpectedChar(
        pos={ line: line.number, column: line.indent + 1, },
        found=':',
      )
    Some(pair) => pair
  }
}

///|
fn split_key_value_opt(content : String) -> (String, String)? {
  let chars = content.to_array()
  let mut in_single = false
  let mut in_double = false
  let mut depth = 0
  let mut i = 0
  let mut found = false
  let key_sb = StringBuilder::new()
  let mut value_start = 0
  let mut prev_space = true
  while i < chars.length() {
    let c = chars[i]
    if prev_space && (c is '&' || c is '!' || c is '*') {
      key_sb.write_char(c)
      i = i + 1
      while i < chars.length() {
        let ch = chars[i]
        if ch.is_whitespace() ||
          ch is ',' ||
          ch is ']' ||
          ch is '}' ||
          ch is '#' {
          break
        }
        key_sb.write_char(ch)
        i = i + 1
      }
      prev_space = false
      continue
    }
    if in_single {
      if c is '\'' {
        in_single = false
      }
      key_sb.write_char(c)
      prev_space = false
      i = i + 1
      continue
    }
    if in_double {
      if c is '"' {
        in_double = false
        key_sb.write_char(c)
        prev_space = false
        i = i + 1
        continue
      }
      if c is '\\' && i + 1 < chars.length() {
        key_sb.write_char(c)
        key_sb.write_char(chars[i + 1])
        prev_space = false
        i = i + 2
        continue
      }
      key_sb.write_char(c)
      prev_space = false
      i = i + 1
      continue
    }
    if c is '\'' && prev_space {
      in_single = true
      key_sb.write_char(c)
      prev_space = false
      i = i + 1
      continue
    }
    if c is '"' && prev_space {
      in_double = true
      key_sb.write_char(c)
      prev_space = false
      i = i + 1
      continue
    }
    if c is '{' || c is '[' {
      depth = depth + 1
      key_sb.write_char(c)
      prev_space = c.is_whitespace()
      i = i + 1
      continue
    }
    if c is '}' || c is ']' {
      if depth > 0 {
        depth = depth - 1
      }
      key_sb.write_char(c)
      i = i + 1
      continue
    }
    if c is ':' {
      if depth == 0 && (i + 1 == chars.length() || chars[i + 1].is_whitespace()) {
        found = true
        value_start = i + 1
        break
      }
    }
    key_sb.write_char(c)
    prev_space = c.is_whitespace()
    i = i + 1
  }
  if !found {
    return None
  }
  let key = key_sb.to_string().trim().to_string()
  let value_sb = StringBuilder::new()
  for j in value_start.. Array[Line] {
  let normalized = input
    .replace_all(old="\r\n", new="\n")
    .replace_all(old="\r", new="\n")
  let lines : Array[Line] = []
  let mut number = 1
  for line_view in normalized.split("\n") {
    let line = line_view.to_string()
    let mut indent = 0
    let mut has_tab_indent = false
    let chars = line.to_array()
    while indent < chars.length() {
      let c = chars[indent]
      if c is ' ' {
        indent = indent + 1
      } else if c is '\t' {
        has_tab_indent = true
        break
      } else {
        break
      }
    }
    let content = line.view(start_offset=indent).to_string()
    let stripped = strip_inline_comment(content)
    let trimmed = stripped.trim_end().to_string()
    lines.push({ number, indent, has_tab_indent, raw: line, content: trimmed, })
    number = number + 1
  }
  lines
}

///|
fn strip_inline_comment(line : String) -> String {
  let chars = line.to_array()
  let mut i = 0
  let mut in_single = false
  let mut in_double = false
  let mut prev_space = true
  let sb = StringBuilder::new()
  while i < chars.length() {
    let c = chars[i]
    if in_single {
      if c is '\'' {
        in_single = false
      }
      sb.write_char(c)
      prev_space = false
      i = i + 1
      continue
    }
    if in_double {
      if c is '"' {
        in_double = false
        sb.write_char(c)
        prev_space = false
        i = i + 1
        continue
      }
      if c is '\\' && i + 1 < chars.length() {
        sb.write_char(c)
        sb.write_char(chars[i + 1])
        prev_space = false
        i = i + 2
        continue
      }
      sb.write_char(c)
      prev_space = false
      i = i + 1
      continue
    }
    if c is '\'' && prev_space {
      in_single = true
      sb.write_char(c)
      prev_space = false
      i = i + 1
      continue
    }
    if c is '"' && prev_space {
      in_double = true
      sb.write_char(c)
      prev_space = false
      i = i + 1
      continue
    }
    if c is '#' && prev_space {
      break
    }
    sb.write_char(c)
    prev_space = c.is_whitespace()
    i = i + 1
  }
  sb.to_string()
}