///|
pub fn parse_yaml(text : String) -> YamlParseResult {
  let lines : Array[YamlLine] = []
  let raw_lines = split_lines_preserve(text)
  for line_number, raw in raw_lines {
    let stripped = strip_yaml_comment(raw)
    if stripped.trim().to_owned() == "" {
      continue
    }
    let mut indent = 0
    let mut has_tab = false
    for ch in stripped {
      if ch == ' ' {
        indent += 1
      } else if ch == '\t' {
        has_tab = true
        break
      } else {
        break
      }
    }
    let content = trim_ascii(stripped)
    if has_tab {
      let line = YamlLine::{ indent, content, line: line_number + 1 }
      let state = YamlState::{ lines: [], index: [0], diagnostics: [] }
      state.error(line, "tabs are not supported for YAML indentation")
      return { value: None, diagnostics: { items: state.diagnostics } }
    }
    if indent % 2 != 0 {
      let line = YamlLine::{ indent, content, line: line_number + 1 }
      let state = YamlState::{ lines: [], index: [0], diagnostics: [] }
      state.error(line, "YAML indentation must use multiples of two spaces")
      return { value: None, diagnostics: { items: state.diagnostics } }
    }
    lines.push(YamlLine::{ indent, content, line: line_number + 1 })
  }
  let state = YamlState::{ lines, index: [0], diagnostics: [] }
  if state.lines.length() == 0 {
    state.error(
      YamlLine::{ indent: 0, content: "", line: 1 },
      "YAML document is empty",
    )
    return { value: None, diagnostics: { items: state.diagnostics } }
  }
  let root_indent = state.lines[0].indent
  let value = parse_yaml_block(state, root_indent)
  if state.index[0] < state.lines.length() {
    state.error(
      state.lines[state.index[0]],
      "unexpected YAML content after root value",
    )
  }
  let final_value = if state.diagnostics.length() == 0 { value } else { None }
  { value: final_value, diagnostics: { items: state.diagnostics } }
}

///|
fn parse_yaml_block(state : YamlState, indent : Int) -> JsonValue? {
  if state.index[0] >= state.lines.length() {
    return Some(JNull)
  }
  let line = state.lines[state.index[0]]
  if line.indent < indent {
    return Some(JNull)
  }
  if line.indent > indent {
    state.error(line, "unexpected indentation")
    return None
  }
  if yaml_starts_sequence(line.content) {
    parse_yaml_sequence(state, indent)
  } else {
    parse_yaml_mapping(state, indent)
  }
}

///|
fn parse_yaml_mapping(state : YamlState, indent : Int) -> JsonValue? {
  let fields : Array[JsonPair] = []
  while state.index[0] < state.lines.length() {
    let line = state.lines[state.index[0]]
    if line.indent < indent {
      break
    }
    if line.indent > indent {
      state.error(line, "mapping entry has inconsistent indentation")
      return None
    }
    if yaml_starts_sequence(line.content) {
      state.error(
        line, "sequence item cannot appear inside a mapping without a key",
      )
      return None
    }
    match yaml_split_key(line.content) {
      None => {
        state.error(line, "expected a YAML key followed by a colon")
        return None
      }
      Some((key, rest)) => {
        if fields.any(fn(item) { item.key == key }) {
          state.error(line, "duplicate YAML mapping key")
          return None
        }
        state.index[0] += 1
        let value = if rest == "" {
          if state.index[0] < state.lines.length() &&
            state.lines[state.index[0]].indent > indent {
            parse_yaml_block(state, state.lines[state.index[0]].indent)
          } else {
            Some(JNull)
          }
        } else {
          parse_yaml_scalar(state, line, rest)
        }
        match value {
          Some(value) => fields.push(JsonPair::new(key, value))
          None => return None
        }
      }
    }
  }
  Some(JObject(fields))
}

///|
fn parse_yaml_sequence(state : YamlState, indent : Int) -> JsonValue? {
  let values : Array[JsonValue] = []
  while state.index[0] < state.lines.length() {
    let line = state.lines[state.index[0]]
    if line.indent < indent {
      break
    }
    if line.indent > indent {
      state.error(line, "sequence item has inconsistent indentation")
      return None
    }
    if !yaml_starts_sequence(line.content) {
      break
    }
    let rest = trim_ascii(
      substring_owned(line.content, 1, line.content.length()),
    )
    state.index[0] += 1
    if rest == "" {
      if state.index[0] < state.lines.length() &&
        state.lines[state.index[0]].indent > indent {
        match parse_yaml_block(state, state.lines[state.index[0]].indent) {
          Some(value) => values.push(value)
          None => return None
        }
      } else {
        values.push(JNull)
      }
    } else if yaml_split_key(rest) is Some((_, _)) {
      let first = parse_yaml_inline_mapping(state, line, indent, rest)
      match first {
        Some(value) => values.push(value)
        None => return None
      }
    } else {
      match parse_yaml_scalar(state, line, rest) {
        Some(value) => values.push(value)
        None => return None
      }
      if state.index[0] < state.lines.length() &&
        state.lines[state.index[0]].indent > indent {
        state.error(
          state.lines[state.index[0]],
          "scalar sequence item cannot have nested content",
        )
        return None
      }
    }
  }
  Some(JArray(values))
}

///|
fn parse_yaml_inline_mapping(
  state : YamlState,
  sequence_line : YamlLine,
  sequence_indent : Int,
  first_text : String,
) -> JsonValue? {
  let fields : Array[JsonPair] = []
  let first = yaml_split_key(first_text).unwrap()
  let (first_key, first_rest) = first
  let first_value = if first_rest == "" {
    if state.index[0] < state.lines.length() &&
      state.lines[state.index[0]].indent > sequence_indent {
      parse_yaml_block(state, state.lines[state.index[0]].indent)
    } else {
      Some(JNull)
    }
  } else {
    parse_yaml_scalar(state, sequence_line, first_rest)
  }
  match first_value {
    Some(value) => fields.push(JsonPair::new(first_key, value))
    None => return None
  }
  if state.index[0] < state.lines.length() &&
    state.lines[state.index[0]].indent > sequence_indent {
    let continuation_indent = state.lines[state.index[0]].indent
    match parse_yaml_mapping(state, continuation_indent) {
      Some(JObject(more)) =>
        for field in more {
          if fields.any(fn(existing) { existing.key == field.key }) {
            state.error(
              sequence_line, "duplicate YAML mapping key in sequence item",
            )
            return None
          }
          fields.push(field)
        }
      Some(_) => {
        state.error(
          sequence_line, "sequence mapping continuation must be a mapping",
        )
        return None
      }
      None => return None
    }
  }
  Some(JObject(fields))
}

///|
fn parse_yaml_scalar(
  state : YamlState,
  line : YamlLine,
  text : String,
) -> JsonValue? {
  if text[0] == '&' || text[0] == '*' {
    state.error(line, "YAML anchors and aliases are not supported")
    return None
  }
  if is_quoted(text) {
    if text[0] == '"' {
      let parsed = parse_json(text)
      match parsed.value {
        Some(JString(value)) => return Some(JString(value))
        _ => {
          state.error(line, "invalid double-quoted YAML scalar")
          return None
        }
      }
    }
    return Some(JString(unquote_single(text)))
  }
  if text == "true" {
    return Some(JBool(true))
  }
  if text == "false" {
    return Some(JBool(false))
  }
  if text == "null" || text == "~" {
    return Some(JNull)
  }
  if text[0] == '[' || text[0] == '{' {
    let parsed = parse_json(text)
    if parsed.value is Some(value) && !parsed.diagnostics.has_errors() {
      return Some(value)
    }
  }
  let number = parse_json(text)
  match number.value {
    Some(JNumber(value)) if !number.diagnostics.has_errors() =>
      Some(JNumber(value))
    _ => Some(JString(text))
  }
}

///|
fn yaml_starts_sequence(text : String) -> Bool {
  text.length() > 0 && text[0] == '-'
}

///|
fn yaml_split_key(text : String) -> (String, String)? {
  let mut quote : Char? = None
  let mut found : (String, String)? = None
  for i, ch in text {
    if ch == '\'' || ch == '"' {
      match quote {
        Some(current) if current == ch => quote = None
        None => quote = Some(ch)
        _ => ()
      }
    } else if ch == ':' && quote is None {
      let key = trim_ascii(substring_owned(text, 0, i))
      let rest = trim_ascii(substring_owned(text, i + 1, text.length()))
      if key != "" {
        found = Some((unquote_key(key), rest))
      }
    }
  }
  found
}

///|
fn strip_yaml_comment(text : String) -> String {
  let builder = StringBuilder::new()
  let mut quote : Char? = None
  let mut previous_space = true
  for ch in text {
    if ch == '\'' || ch == '"' {
      match quote {
        Some(current) if current == ch => quote = None
        None => quote = Some(ch)
        _ => ()
      }
      builder.write_char(ch)
      previous_space = false
    } else if ch == '#' && quote is None && previous_space {
      break
    } else {
      builder.write_char(ch)
      previous_space = ch == ' ' || ch == '\t'
    }
  }
  builder.to_string()
}

///|
fn substring_owned(text : String, start : Int, end : Int) -> String {
  let builder = StringBuilder::new()
  let mut index = 0
  for ch in text {
    if index >= start && index < end {
      builder.write_char(ch)
    }
    index += 1
  }
  builder.to_string()
}

///|
fn unquote_single(text : String) -> String {
  if text.length() < 2 {
    text
  } else {
    let inner = substring_owned(text, 1, text.length() - 1)
    inner.replace(old="''", new="'")
  }
}

///|
fn unquote_key(text : String) -> String {
  if is_quoted(text) {
    if text[0] == '\'' {
      unquote_single(text)
    } else {
      match parse_json(text).value {
        Some(JString(value)) => value
        _ => text
      }
    }
  } else {
    text
  }
}