///|
/// YAML data structure representation
pub(all) enum Yaml {
  /// Float types
  Real(Double, repr~ : String)
  /// YAML int is stored as Int64.
  Integer(Int64)
  /// YAML scalar.
  String(String)
  /// YAML bool, e.g. `true` or `false`.
  Boolean(Bool)
  /// YAML array, can be accessed as an `Array`.
  Array(Array[Yaml])
  /// YAML map, can be accessed as a `Map`.
  /// 
  /// Insertion order will match the order of insertion into the map.
  ///
  /// Note that YAML keys can be of any type, but restrict them to String here for simplicity.
  Map(Map[String, Yaml])
  /// YAML null, e.g. `null` or `~`.
  Null
  /// Accessing a nonexistent node via indexing returns `BadValue`. This
  /// simplifies error handling in the calling code. Invalid type conversion also
  /// returns `BadValue`.
  BadValue
} derive(Eq, Debug)

///|
pub impl ToJson for Yaml with fn to_json(self) {
  // assume input not contain complicate yaml feature
  match self {
    Null => Json::null()
    Map(yaml_map) => Json::object(yaml_map.map((_, yaml) => yaml.to_json()))
    Array(yaml_array) => Json::array(yaml_array.map(yaml => yaml.to_json()))
    Boolean(bool) => Json::boolean(bool)
    String(str) => Json::string(str)
    Integer(i64) => Json::number(i64.to_double())
    Real(f64, repr~) => Json::number(f64, repr~)
    BadValue => panic()
  }
}

///|
fn Yaml::from_str(v : String) -> Yaml {
  fn yaml_from_str(v : String) -> Yaml {
    if (try @string.parse_int64(v) catch {
        err => Err(err)
      } noraise {
        val => Ok(val)
      })
      is Ok(integer) {
      Yaml::Integer(integer)
    } else if parse_double(v) is Some(d) {
      Yaml::Real(d, repr=v)
    } else {
      Yaml::String(v)
    }
  }
  match v {
    ['0', 'x', .. number] =>
      Yaml::Integer(@string.parse_int64(number, base=16)) catch {
        _ => yaml_from_str(v)
      }
    ['0', 'o', .. number] =>
      Yaml::Integer(@string.parse_int64(number, base=8)) catch {
        _ => yaml_from_str(v)
      }
    ['+', .. number] =>
      Yaml::Integer(@string.parse_int64(number)) catch {
        _ => yaml_from_str(v)
      }
    "" | "~" | "null" => Yaml::Null
    "true" | "True" | "TRUE" => Yaml::Boolean(true)
    "false" | "False" | "FALSE" => Yaml::Boolean(false)
    v => yaml_from_str(v)
  }
}

///|
test "simple string to yaml" {
  debug_inspect(Yaml::from_str("42"), content="Integer(42)")
  debug_inspect(Yaml::from_str("0x2A"), content="Integer(42)")
  debug_inspect(Yaml::from_str("0o52"), content="Integer(42)")
  debug_inspect(Yaml::from_str("~"), content="Null")
  debug_inspect(Yaml::from_str("true"), content="Boolean(true)")
  debug_inspect(Yaml::from_str("True"), content="Boolean(true)")
  debug_inspect(Yaml::from_str("TRUE"), content="Boolean(true)")
  debug_inspect(Yaml::from_str("false"), content="Boolean(false)")
  debug_inspect(Yaml::from_str("False"), content="Boolean(false)")
  debug_inspect(Yaml::from_str("FALSE"), content="Boolean(false)")
  assert_true(Yaml::from_str("3.14") is Yaml::Real(_))
  assert_true(Yaml::from_str("hello") is Yaml::String(_))
}

///|
fn parse_double(v : StringView) -> Double? {
  match v {
    ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" =>
      Some(@double.infinity)
    "-.inf" | "-.Inf" | "-.INF" => Some(@double.neg_infinity)
    ".nan" | ".NaN" | ".NAN" => Some(@double.not_a_number)
    v if v.iter().any(ch => ch.is_ascii_digit()) =>
      Some(@string.parse_double(v)) catch {
        _ => None
      }
    _ => None
  }
}

///|
priv struct YamlLoader {
  /// The different YAML documents that are loaded.
  docs : Array[Yaml]
  /// stack of (current node, anchor_id)
  doc_stack : Array[(Yaml, Int)]
  key_stack : Array[String]
  anchor_map : @sorted_map.SortedMap[Int, Yaml]
  mut error : YamlError?
}

///|
pub fn Yaml::load_from_parser(parser : Parser) -> Array[Yaml] raise YamlError {
  let loader = YamlLoader::{
    docs: [],
    doc_stack: [],
    key_stack: [],
    anchor_map: @sorted_map.SortedMap([]),
    error: None,
  }
  parser.load(loader, true)
  if loader.error is Some(err) {
    raise err
  } else {
    loader.docs
  }
}

///|
pub fn Yaml::load_from_string(
  source : StringView,
) -> Array[Yaml] raise YamlError {
  let parser = Parser::new(source)
  Yaml::load_from_parser(parser)
}

///|
impl MarkedEventReceiver for YamlLoader with fn on_event(self, ev, mark) {
  if self.error is Some(_) {
    return
  }
  self.on_event_impl(ev, mark) catch {
    YamlError(_) as e => self.error = Some(e)
  }
}

///|
fn YamlLoader::on_event_impl(
  self : YamlLoader,
  ev : Event,
  mark : Marker,
) -> Unit raise YamlError {
  match ev {
    Event::DocumentStart | Event::StreamStart | Event::StreamEnd => () // do nothing
    Event::DocumentEnd =>
      match self.doc_stack.length() {
        // empty document
        0 => self.docs.push(Yaml::BadValue)
        1 => self.docs.push(self.doc_stack.pop().unwrap().0)
        _ => panic()
      }
    Event::SequenceStart(id~, ..) => self.doc_stack.push((Yaml::Array([]), id))
    Event::SequenceEnd => {
      let node = self.doc_stack.pop().unwrap()
      self.insert_new_node(node, mark)
    }
    Event::MappingStart(id~, ..) => {
      self.doc_stack.push((Yaml::Map({}), id))
      self.key_stack.push("") // place holder
    }
    Event::MappingEnd => {
      guard self.key_stack.pop() is Some(_)
      let node = self.doc_stack.pop().unwrap()
      self.insert_new_node(node, mark)
    }
    Event::Scalar(value~, style~, id~, tag~) => {
      let node = if style != TScalarStyle::Plain {
        Yaml::String(value)
      } else if tag is Some({ handle, suffix }) {
        if handle == "tag:yaml.org,2002:" {
          match suffix {
            "bool" =>
              match value {
                "true" | "True" | "TRUE" => Yaml::Boolean(true)
                "false" | "False" | "FALSE" => Yaml::Boolean(false)
                _ => Yaml::BadValue
              }
            "int" =>
              Yaml::Integer(@string.parse_int64(value)) catch {
                _ => Yaml::BadValue
              }
            "float" =>
              match parse_double(value) {
                Some(d) => Yaml::Real(d, repr=value)
                None => Yaml::BadValue
              }
            "null" =>
              match value {
                "~" | "null" => Yaml::Null
                _ => Yaml::BadValue
              }
            _ => Yaml::String(value)
          }
        } else {
          Yaml::String(value)
        }
      } else {
        // Datatype is not specified, or unrecognized
        Yaml::from_str(value)
      }
      self.insert_new_node((node, id), mark)
    }
    Event::Alias(id~) => {
      let n = match self.anchor_map.get(id) {
        Some(v) => v
        None => Yaml::BadValue
      }
      self.insert_new_node((n, 0), mark)
    }
  }
}

///|
fn YamlLoader::insert_new_node(
  self : YamlLoader,
  node : (Yaml, Int),
  mark : Marker,
) -> Unit raise YamlError {
  // valid anchor id starts from 1
  if node.1 > 0 {
    self.anchor_map[node.1] = node.0
  }
  if self.doc_stack.is_empty() {
    self.doc_stack.push(node)
  } else {
    let parent = self.doc_stack.last().unwrap()
    match parent {
      (Array(v), _) => v.push(node.0)
      (Map(m), _) => {
        let cur_key = self.key_stack.last().unwrap()
        if cur_key.is_empty() {
          // current node is a key
          if node.0 is String(key) {
            self.key_stack[self.key_stack.length() - 1] = key
          } else {
            raise YamlError::YamlError(
              mark~,
              info="yaml.mbt doesn't support non-string key",
            )
          }
        } else {
          // current node is a value
          let new_key = cur_key
          self.key_stack[self.key_stack.length() - 1] = ""
          if m.contains(new_key) {
            raise YamlError::YamlError(
              mark~,
              info="\{new_key}: duplicated key in mapping",
            )
          } else {
            m[new_key] = node.0
          }
        }
      }
      _ => panic()
    }
  }
}

///|
/// The YAML serializer.
///
/// This is a simplified emitter that matches the parser features supported by
/// this package.
///
/// # Example
/// ```mbt nocheck
/// let input = "a: b\nc: d"
/// let docs = Yaml::load_from_string(input)
/// let emitter = YamlEmitter::new()
/// emitter.dump(docs[0])
/// let output = emitter.to_string()
/// // output == "---\na: b\nc: d"
/// ```
struct YamlEmitter {
  writer : StringBuilder
  best_indent : Int
  mut compact : Bool
  mut level : Int
  mut multiline_strings : Bool
}

///|
/// Create a new emitter serializing into `writer`.
pub fn YamlEmitter::new(
  writer? : StringBuilder = StringBuilder::new(),
) -> YamlEmitter {
  YamlEmitter::{
    writer,
    best_indent: 2,
    compact: true,
    level: -1,
    multiline_strings: false,
  }
}

///|
/// Set "compact inline notation" on or off for block sequences and mappings.
///
/// In this form, blocks cannot have any properties (such as anchors or tags),
/// which should be OK, because this emitter doesn't emit those anyways.
pub fn YamlEmitter::compact(self : YamlEmitter, compact : Bool) -> Unit {
  self.compact = compact
}

///|
/// Render strings containing multiple lines in literal block style.
///
/// # Example
/// ```mbt nocheck
/// let input = "{foo: \"bar!\\nbar!\", baz: 42}"
/// let parsed = Yaml::load_from_string(input)
/// let emitter = YamlEmitter::new()
/// emitter.multiline_strings(true)
/// emitter.dump(parsed[0])
/// let output = emitter.to_string()
/// // output is:
/// // ---
/// // foo: |-
/// //   bar!
/// //   bar!
/// // baz: 42
/// ```
pub fn YamlEmitter::multiline_strings(
  self : YamlEmitter,
  multiline_strings : Bool,
) -> Unit {
  self.multiline_strings = multiline_strings
}

///|
/// Dump a YAML document into the internal buffer.
pub fn YamlEmitter::dump(self : YamlEmitter, doc : Yaml) -> Unit {
  self.writer.write_string("---")
  self.writer.write_char('\n')
  self.level = -1
  self.emit_node(doc)
}

///|
/// Get the current emitted output as a string.
pub fn YamlEmitter::to_string(self : YamlEmitter) -> String {
  self.writer.to_string()
}

///|
/// Convenience helper to dump a `Yaml` value into a string using default settings.
pub fn Yaml::dump(self : Yaml) -> String {
  let emitter = YamlEmitter::new()
  emitter.dump(self)
  emitter.to_string()
}

///|
fn YamlEmitter::write_indent(self : YamlEmitter) -> Unit {
  if self.level <= 0 {
    return
  }
  for _ in 0.. Unit {
  match node {
    Array(values) => self.emit_array(values)
    Map(values) => self.emit_map(values)
    String(value) =>
      if self.multiline_strings &&
        value.contains_char('\n') &&
        is_valid_literal_block_scalar(value) {
        self.emit_literal_block(value)
      } else if need_quotes(value) {
        escape_str(self.writer, value)
      } else {
        self.writer.write_string(value)
      }
    Boolean(value) =>
      if value {
        self.writer.write_string("true")
      } else {
        self.writer.write_string("false")
      }
    Integer(value) => self.writer.write_string(value.to_string())
    Real(value, repr~) =>
      if repr.is_empty() {
        self.writer.write_string(value.to_string())
      } else {
        self.writer.write_string(repr)
      }
    Null | BadValue => self.writer.write_char('~')
  }
}

///|
fn YamlEmitter::emit_literal_block(self : YamlEmitter, value : String) -> Unit {
  let len = value.length()
  let ends_with_newline = len > 0 && value[len - 1] == ('\n' : UInt16)
  if ends_with_newline {
    self.writer.write_char('|')
  } else {
    self.writer.write_string("|-")
  }
  self.level += 1
  let lines = value.split("\n").to_array()
  if ends_with_newline && !lines.is_empty() && lines.last().unwrap().is_empty() {
    ignore(lines.pop())
  }
  for line in lines {
    self.writer.write_char('\n')
    self.write_indent()
    self.writer.write_view(line)
  }
  self.level -= 1
}

///|
fn YamlEmitter::emit_array(self : YamlEmitter, values : Array[Yaml]) -> Unit {
  if values.is_empty() {
    self.writer.write_string("[]")
  } else {
    self.level += 1
    for index, value in values {
      if index > 0 {
        self.writer.write_char('\n')
        self.write_indent()
      }
      self.writer.write_char('-')
      self.emit_val(true, value)
    }
    self.level -= 1
  }
}

///|
fn YamlEmitter::emit_map(
  self : YamlEmitter,
  values : Map[String, Yaml],
) -> Unit {
  if values.is_empty() {
    self.writer.write_string("{}")
  } else {
    self.level += 1
    values.eachi((index, key, value) => {
      if index > 0 {
        self.writer.write_char('\n')
        self.write_indent()
      }
      self.emit_node(Yaml::String(key))
      self.writer.write_char(':')
      self.emit_val(false, value)
    })
    self.level -= 1
  }
}

///|
/// Emit a YAML as a mapping or sequence value, following ':' or '-'.
///
/// If `inline` is true, the preceding characters are distinct and short
/// enough to respect the compact flag.
fn YamlEmitter::emit_val(
  self : YamlEmitter,
  inline : Bool,
  value : Yaml,
) -> Unit {
  match value {
    Array(values) => {
      if (inline && self.compact) || values.is_empty() {
        self.writer.write_char(' ')
      } else {
        self.writer.write_char('\n')
        self.level += 1
        self.write_indent()
        self.level -= 1
      }
      self.emit_array(values)
    }
    Map(values) => {
      if (inline && self.compact) || values.is_empty() {
        self.writer.write_char(' ')
      } else {
        self.writer.write_char('\n')
        self.level += 1
        self.write_indent()
        self.level -= 1
      }
      self.emit_map(values)
    }
    _ => {
      self.writer.write_char(' ')
      self.emit_node(value)
    }
  }
}

///|
fn escape_str(writer : StringBuilder, value : String) -> Unit {
  writer.write_char('"')
  for ch in value {
    match ch {
      '"' => writer.write_string("\\\"")
      '\\' => writer.write_string("\\\\")
      '\u{0008}' => writer.write_string("\\b")
      '\t' => writer.write_string("\\t")
      '\n' => writer.write_string("\\n")
      '\u{000c}' => writer.write_string("\\f")
      '\r' => writer.write_string("\\r")
      _ => {
        let code = ch.to_int()
        if code <= 0x1f || code == 0x7f {
          write_hex_escape(writer, code)
        } else {
          writer.write_char(ch)
        }
      }
    }
  }
  writer.write_char('"')
}

///|
fn write_hex_escape(writer : StringBuilder, value : Int) -> Unit {
  writer.write_string("\\u")
  writer.write_char(hex_digit((value >> 12) & 0xf))
  writer.write_char(hex_digit((value >> 8) & 0xf))
  writer.write_char(hex_digit((value >> 4) & 0xf))
  writer.write_char(hex_digit(value & 0xf))
}

///|
fn hex_digit(value : Int) -> Char {
  match value {
    0 => '0'
    1 => '1'
    2 => '2'
    3 => '3'
    4 => '4'
    5 => '5'
    6 => '6'
    7 => '7'
    8 => '8'
    9 => '9'
    10 => 'a'
    11 => 'b'
    12 => 'c'
    13 => 'd'
    14 => 'e'
    15 => 'f'
    _ => panic()
  }
}

///|
fn is_valid_literal_block_scalar(value : String) -> Bool {
  value
  .iter()
  .all(ch => {
    ch == '\t' ||
    ch == '\n' ||
    ch is ('\u{0020}'..='\u{007e}') ||
    ch == '\u{0085}' ||
    ch is ('\u{00a0}'..='\u{d7ff}')
  })
}

///|
fn need_quotes_spaces(value : String) -> Bool {
  let len = value.length()
  value.trim_start(chars=" ").length() != len ||
  value.trim_end(chars=" ").length() != len
}

///|
/// Check if the string requires quoting.
///
/// Strings starting with any of the following characters must be quoted:
/// `:`, `&`, `*`, `?`, `|`, `-`, `<`, `>`, `=`, `!`, `%`, `@`.
///
/// Strings containing any of the following characters must be quoted:
/// `{`, `}`, `[`, `]`, `,`, `#`, `` ` ``.
///
/// If the string contains any of the following control characters, it must be
/// escaped with double quotes:
/// `\0`, `\x01`, `\x02`, `\x03`, `\x04`, `\x05`, `\x06`, `\a`, `\b`, `\t`,
/// `\n`, `\v`, `\f`, `\r`, `\x0e`, `\x0f`, `\x10`, `\x11`, `\x12`, `\x13`,
/// `\x14`, `\x15`, `\x16`, `\x17`, `\x18`, `\x19`, `\x1a`, `\e`, `\x1c`,
/// `\x1d`, `\x1e`, `\x1f`, `\N`, `\_`, `\L`, `\P`.
///
/// Finally, there are other cases when the strings must be quoted:
/// - When the string is true or false.
/// - When the string is null or "~".
/// - When the string looks like a number, such as integers, floats, and
///   exponential numbers.
/// - When the string looks like a date (e.g. 2014-12-31).
fn need_quotes(value : String) -> Bool {
  if value.is_empty() || need_quotes_spaces(value) {
    return true
  }
  match value.get_char(0) {
    Some(ch) if ch
      is ('&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@') =>
      return true
    _ => ()
  }
  if value
    .iter()
    .any(ch => {
      ch is (':' | '{' | '}' | '[' | ']' | ',' | '#' | '`' | '"' | '\'' | '\\') ||
      ch.to_int() <= 0x06 ||
      ch == '\t' ||
      ch == '\n' ||
      ch == '\r' ||
      (ch.to_int() >= 0x0e && ch.to_int() <= 0x1a) ||
      (ch.to_int() >= 0x1c && ch.to_int() <= 0x1f)
    }) {
    return true
  }
  if [
      "true", "false", "True", "False", "TRUE", "FALSE", "null", "Null", "NULL",
      "~", "y", "Y", "n", "N", "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE",
      "False", "FALSE", "on", "On", "ON", "off", "Off", "OFF",
    ].contains(value) {
    return true
  }
  match value.get_char(0) {
    Some('.') => return true
    _ => ()
  }
  if value is ['0', 'x', ..] {
    return true
  }
  try ignore(@string.parse_int64(value)) catch {
    _ =>
      try ignore(@string.parse_double(value)) catch {
        _ => false
      } noraise {
        _ => true
      }
  } noraise {
    _ => true
  }
}