///|
pub(all) enum KV3Value {
  KV3Null
  KV3Bool(Bool)
  KV3Integer(String)
  KV3Float(String)
  KV3String(String)
  KV3Blob(Bytes)
  KV3Array(Array[KV3Value])
  KV3Object(Array[KV3Entry])
  KV3Flag(String, KV3Value)
} derive(Eq, Debug)

///|
pub(all) struct KV3Entry {
  key : String
  value : KV3Value
} derive(Eq, Debug)

///|
pub(all) struct KV3Header {
  format_name : String
  format_id : String
} derive(Eq, Debug)

///|
pub(all) struct KV3Document {
  header : KV3Header?
  root : KV3Value
} derive(Eq, Debug)

///|
pub extend KV3Value with Eq::{equal, not_equal}

///|
pub extend KV3Value with @debug.Debug::{to_repr}

///|
pub extend KV3Entry with Eq::{equal, not_equal}

///|
pub extend KV3Entry with @debug.Debug::{to_repr}

///|
pub extend KV3Header with Eq::{equal, not_equal}

///|
pub extend KV3Header with @debug.Debug::{to_repr}

///|
pub extend KV3Document with Eq::{equal, not_equal}

///|
pub extend KV3Document with @debug.Debug::{to_repr}

///|
pub fn generic_kv3_header() -> KV3Header {
  { format_name: "generic", format_id: "7412167c-06e9-4698-aff2-e63eb59037e7", }
}

///|
priv struct KV3Reader {
  chars : Array[Char]
  options : ParseOptions
  mut pos : Int
  mut line : Int
  mut column : Int
  mut nodes : Int
}

///|
fn KV3Reader::fail(self : KV3Reader, message : String) -> Unit raise ParseError {
  raise ParseError({
    code: "kv3_syntax",
    message,
    offset: self.pos,
    line: self.line,
    column: self.column,
  })
}

///|
fn KV3Reader::peek(self : KV3Reader) -> Char? {
  self.chars.get(self.pos)
}

///|
fn KV3Reader::advance(self : KV3Reader) -> Unit raise ParseError {
  if self.pos >= self.chars.length() {
    self.fail("Unexpected end of KV3")
  }
  let c = self.chars[self.pos]
  if c.to_int() == 0 {
    self.fail("NUL is not accepted in KV3 text")
  }
  self.pos += 1
  if c == '\r' {
    self.line += 1
    self.column = 1
  } else if c == '\n' {
    if self.pos < 2 || self.chars[self.pos - 2] != '\r' {
      self.line += 1
    }
    self.column = 1
  } else {
    self.column += 1
  }
}

///|
fn KV3Reader::space(self : KV3Reader) -> Unit raise ParseError {
  while self.peek() is Some(c) {
    if is_space(c) {
      self.advance()
      continue
    }
    if c == '/' && self.chars.get(self.pos + 1) == Some('/') {
      self.advance()
      self.advance()
      let mut previous = ' '
      while self.peek() is Some(next) {
        if next == '\n' || next == '\r' {
          self.advance()
          if next == '\r' && self.peek() == Some('\n') {
            self.advance()
          }
          if previous != '\\' {
            break
          }
          previous = ' '
        } else {
          previous = next
          self.advance()
        }
      }
    } else if c == '/' && self.chars.get(self.pos + 1) == Some('*') {
      self.advance()
      self.advance()
      while !(self.peek() == Some('*') &&
            self.chars.get(self.pos + 1) == Some('/')) {
        if self.peek() is None {
          self.fail("Unclosed block comment")
        }
        self.advance()
      }
      self.advance()
      self.advance()
    } else {
      break
    }
  }
}

///|
fn KV3Reader::take(self : KV3Reader, wanted : Char) -> Unit raise ParseError {
  self.space()
  if self.peek() != Some(wanted) {
    self.fail("Expected '\{wanted}'")
  }
  self.advance()
}

///|
fn KV3Reader::word(self : KV3Reader) -> String raise ParseError {
  self.space()
  let begin = self.pos
  while self.peek() is Some(c) &&
        c.to_int() > 32 &&
        c.to_int() < 128 &&
        !"{}[]=,'\":|;".contains(c.to_string()) {
    self.advance()
    if self.pos - begin > self.options.max_token_chars {
      self.fail("Token exceeds KV3 limit")
    }
  }
  if self.pos == begin {
    self.fail("Expected an unquoted KV3 token")
  }
  String::from_array(self.chars[begin:self.pos].to_owned())
}

///|
fn kv3_identifier(text : String) -> Bool {
  !text.is_empty() &&
  !(text[0] >= '0' && text[0] <= '9') &&
  text.all(c => {
    (c >= 'a' && c <= 'z') ||
    (c >= 'A' && c <= 'Z') ||
    (c >= '0' && c <= '9') ||
    c == '_' ||
    c == '.'
  })
}

///|
fn kv3_hex(c : Char) -> Int? {
  if c >= '0' && c <= '9' {
    Some(c.to_int() - 48)
  } else if c >= 'a' && c <= 'f' {
    Some(c.to_int() - 87)
  } else if c >= 'A' && c <= 'F' {
    Some(c.to_int() - 55)
  } else {
    None
  }
}

///|
fn kv3_guid(text : String) -> Bool {
  let chars = text.to_array()
  chars.length() == 36 &&
  chars
  .mapi((i, c) => {
    if [8, 13, 18, 23].contains(i) {
      c == '-'
    } else {
      kv3_hex(c) is Some(_)
    }
  })
  .all(b => b)
}

///|
fn KV3Reader::expect_word(
  self : KV3Reader,
  expected : String,
) -> Unit raise ParseError {
  if !self.word().equal_ignore_ascii_case(expected) {
    self.fail("Expected header token " + expected)
  }
}

///|
fn KV3Reader::header(self : KV3Reader) -> KV3Header raise ParseError {
  self.expect_word("")
  { format_name, format_id, }
}

///|
fn KV3Reader::string(self : KV3Reader) -> String raise ParseError {
  self.space()
  let quote = self.peek().unwrap()
  self.advance()
  if quote == '"' &&
    self.peek() == Some('"') &&
    self.chars.get(self.pos + 1) == Some('"') {
    self.advance()
    self.advance()
    if self.peek() == Some('\r') {
      self.advance()
    }
    if self.peek() != Some('\n') {
      self.fail("Triple quotes require an opening newline")
    }
    self.advance()
    let begin = self.pos
    let mut start_of_line = true
    let mut previous = ' '
    while true {
      if start_of_line &&
        self.peek() == Some('"') &&
        self.chars.get(self.pos + 1) == Some('"') &&
        self.chars.get(self.pos + 2) == Some('"') {
        let mut end = self.pos
        if end > begin && self.chars[end - 1] == '\n' {
          end -= 1
        }
        if end > begin && self.chars[end - 1] == '\r' {
          end -= 1
        }
        let value = String::from_array(self.chars[begin:end].to_owned())
          .replace_all(old="\r\n", new="\n")
          .replace_all(old="\\\"\"\"", new="\"\"\"")
        self.advance()
        self.advance()
        self.advance()
        return value
      }
      if self.peek() is None {
        self.fail("Unclosed multiline string")
      }
      let c = self.peek().unwrap()
      self.advance()
      start_of_line = c == '\n' &&
        previous != '\\' &&
        !(previous == '\r' && self.pos >= 3 && self.chars[self.pos - 3] == '\\')
      previous = c
      if self.pos - begin > self.options.max_token_chars {
        self.fail("Multiline string exceeds KV3 token limit")
      }
    }
  }
  let output = StringBuilder()
  let mut size = 0
  while self.peek() is Some(c) {
    self.advance()
    if c == quote {
      return output.to_string()
    }
    if c == '\\' {
      guard self.peek() is Some(escaped) else {
        self.fail("Incomplete string escape")
        return ""
      }
      self.advance()
      output.write_char(
        match escaped {
          'n' => '\n'
          't' => '\t'
          _ => escaped
        },
      )
    } else {
      output.write_char(c)
    }
    size += 1
    if size > self.options.max_token_chars {
      self.fail("String exceeds KV3 token limit")
    }
  }
  self.fail("Unclosed KV3 string")
  ""
}

///|
fn kv3_flag(text : String) -> Bool {
  [
    "resource", "resource_name", "panorama", "soundevent", "subclass", "entity_name",
    "localize",
  ].contains(text.to_lower())
}

///|
fn kv3_number(text : String) -> KV3Value raise WriteError {
  let lower = text.to_lower()
  if ["nan", "inf", "+inf", "-inf"].contains(lower) {
    return KV3Float(text)
  }
  let digits = if text.has_prefix("-") || text.has_prefix("+") {
    text[1:].to_owned()
  } else {
    text
  }
  if decimal_id(digits) {
    try {
      if text.has_prefix("-") {
        @strconv.parse_int64(text[:]) |> ignore
      } else {
        @strconv.parse_uint64(digits[:]) |> ignore
      }
    } catch {
      _ => raise InvalidDocument("KV3 integer exceeds 64-bit range")
    }
    return KV3Integer(text)
  }
  if text.is_empty() ||
    !text.all(c => "+-.0123456789eE".contains(c.to_string())) {
    raise InvalidDocument("Invalid KV3 number")
  }
  let number = @strconv.parse_double(text[:]) catch {
    _ => raise InvalidDocument("Invalid KV3 floating-point literal")
  }
  if number.is_inf() || number.is_nan() {
    raise InvalidDocument(
      "Floating-point overflow; use an explicit inf literal",
    )
  }
  KV3Float(text)
}

///|
fn KV3Reader::value(self : KV3Reader, depth : Int) -> KV3Value raise {
  self.space()
  if depth > self.options.max_depth {
    self.fail("KV3 depth limit exceeded")
  }
  self.nodes += 1
  if self.nodes > self.options.max_entries {
    self.fail("KV3 node limit exceeded")
  }
  match self.peek() {
    Some('{') => {
      self.advance()
      let entries : Array[KV3Entry] = []
      let keys : Map[String, Bool] = Map([])
      while true {
        self.space()
        if self.peek() == Some('}') {
          self.advance()
          break
        }
        let quoted = self.peek() == Some('"') || self.peek() == Some('\'')
        let key = if quoted { self.string() } else { self.word() }
        if !quoted && !kv3_identifier(key) {
          self.fail("Unquoted object keys must be identifiers")
        }
        if keys.contains(key) {
          self.fail("Duplicate KV3 object key")
        }
        keys[key] = true
        self.take('=')
        entries.push({ key, value: self.value(depth + 1), })
      }
      KV3Object(entries)
    }
    Some('[') => {
      self.advance()
      let values : Array[KV3Value] = []
      self.space()
      while self.peek() != Some(']') {
        values.push(self.value(depth + 1))
        self.space()
        if self.peek() == Some(']') {
          break
        }
        self.take(',')
        self.space()
      }
      self.take(']')
      KV3Array(values)
    }
    Some('#') => {
      self.advance()
      self.take('[')
      let output = @buffer.Buffer()
      while true {
        self.space()
        if self.peek() == Some(']') {
          self.advance()
          break
        }
        let token = self.word().to_array()
        guard token.length() == 2 &&
          kv3_hex(token[0]) is Some(a) &&
          kv3_hex(token[1]) is Some(b) else {
          self.fail("Blob bytes require two hexadecimal digits")
          return KV3Null
        }
        if output.length() >= self.options.max_token_chars {
          self.fail("Blob exceeds KV3 token limit")
        }
        output.write_byte((a * 16 + b).to_byte())
      }
      KV3Blob(output.to_bytes())
    }
    Some('"') | Some('\'') => KV3String(self.string())
    None => {
      self.fail("Expected a KV3 value")
      KV3Null
    }
    _ => {
      let token = self.word()
      self.space()
      if self.peek() == Some(':') || self.peek() == Some('|') {
        if !kv3_flag(token) {
          self.fail("Unknown KV3 flag")
        }
        self.advance()
        let value = self.value(depth + 1)
        if value is KV3Flag(_, _) {
          self.fail("A value cannot have stacked flags")
        }
        return KV3Flag(token.to_lower(), value)
      }
      match token.to_lower() {
        "null" => KV3Null
        "true" => KV3Bool(true)
        "false" => KV3Bool(false)
        _ =>
          kv3_number(token) catch {
            _ => {
              self.fail("Invalid KV3 literal; strings must be quoted")
              KV3Null
            }
          }
      }
    }
  }
}

///|
/// Text KV3 only. Limits reuse ParseOptions; KV1 condition/literal modes are invalid.
pub fn parse_kv3(
  input : String,
  options? : ParseOptions = ParseOptions::default(),
  allow_headerless? : Bool = false,
) -> KV3Document raise {
  parse("", options~) |> ignore
  if !options.escape_sequences || options.conditions is Some(_) {
    raise InvalidDocument("KV3 does not use KV1 literal or condition options")
  }
  if input.char_length() > options.max_input_chars {
    raise InvalidDocument("KV3 input exceeds character budget")
  }
  let reader = KV3Reader::{
    chars: input.to_array(),
    options,
    pos: 0,
    line: 1,
    column: 1,
    nodes: 0,
  }
  if reader.peek() == Some('\uFEFF') {
    reader.advance()
  }
  reader.space()
  let header = if reader.peek() == Some('<') {
    Some(reader.header())
  } else if allow_headerless {
    None
  } else {
    reader.fail("Expected KV3 text header")
    None
  }
  let root = reader.value(0)
  reader.space()
  if reader.peek() is Some(_) {
    reader.fail("Trailing data after root value")
  }
  { header, root, }
}

///|
fn kv3_write_string(
  output : StringBuilder,
  text : String,
) -> Unit raise WriteError {
  if text.char_length() > 1048576 {
    raise InvalidDocument("KV3 string exceeds token limit")
  }
  output.write_char('"')
  for c in text {
    match c {
      '"' => output.write_string("\\\"")
      '\\' => output.write_string("\\\\")
      '\n' => output.write_string("\\n")
      '\t' => output.write_string("\\t")
      _ => {
        if c.to_int() == 0 {
          raise InvalidDocument("NUL in KV3 string")
        }
        output.write_char(c)
      }
    }
  }
  output.write_char('"')
}

///|
fn kv3_write_value(
  output : StringBuilder,
  value : KV3Value,
  depth : Int,
  count : Ref[Int],
) -> Unit raise {
  count.val += 1
  if depth > 128 || count.val > 100000 {
    raise InvalidDocument("KV3 writer resource limit exceeded")
  }
  match value {
    KV3Null => output.write_string("null")
    KV3Bool(b) => output.write_string(if b { "true" } else { "false" })
    KV3String(s) => kv3_write_string(output, s)
    KV3Integer(text) => {
      if !(kv3_number(text) is KV3Integer(_)) {
        raise InvalidDocument("Invalid KV3 integer")
      }
      output.write_string(text)
    }
    KV3Float(text) => {
      if !(kv3_number(text) is KV3Float(_)) {
        raise InvalidDocument(
          "Float literals must contain a decimal point, exponent, or special keyword",
        )
      }
      output.write_string(text)
    }
    KV3Flag(flag, value) => {
      if !kv3_flag(flag) || value is KV3Flag(_, _) {
        raise InvalidDocument("Invalid KV3 flag")
      }
      output.write_string(flag.to_lower() + ":")
      kv3_write_value(output, value, depth + 1, count)
    }
    KV3Blob(bytes) => {
      if bytes.length() > 1048576 {
        raise InvalidDocument("KV3 blob exceeds token limit")
      }
      output.write_string("#[")
      for byte in bytes {
        let digits = "0123456789abcdef"
        output.write_char(digits[byte.to_int() / 16].to_char().unwrap())
        output.write_char(digits[byte.to_int() % 16].to_char().unwrap())
        output.write_char(' ')
      }
      output.write_char(']')
    }
    KV3Array(values) => {
      output.write_string("[")
      for i in 0.. 0 {
          output.write_string(", ")
        }
        kv3_write_value(output, values[i], depth + 1, count)
      }
      output.write_string("]")
    }
    KV3Object(entries) => {
      output.write_string("{\n")
      let seen : Map[String, Bool] = Map([])
      for entry in entries {
        if seen.contains(entry.key) {
          raise InvalidDocument("Duplicate KV3 key")
        }
        seen[entry.key] = true
        write_indent(output, depth + 1)
        kv3_write_string(output, entry.key)
        output.write_string(" = ")
        kv3_write_value(output, entry.value, depth + 1, count)
        output.write_string("\n")
      }
      write_indent(output, depth)
      output.write_string("}")
    }
  }
}

///|
pub fn stringify_kv3(document : KV3Document) -> String raise {
  kv3_check_size(document.root, 0, { val: 8387584, }, { val: 0, })
  let output = StringBuilder()
  if document.header is Some(header) {
    if header.format_name.length() > 128 ||
      !kv3_identifier(header.format_name) ||
      !kv3_guid(header.format_id) ||
      (
        header.format_name.equal_ignore_ascii_case("generic") &&
        !header.format_id.equal_ignore_ascii_case(
          generic_kv3_header().format_id,
        )
      ) {
      raise InvalidDocument("Invalid KV3 format header")
    }
    output.write_string(
      "\n",
    )
  }
  kv3_write_value(output, document.root, 0, { val: 0, })
  output.write_string("\n")
  let text = output.to_string()
  parse_kv3(text, allow_headerless=document.header is None) |> ignore
  text
}

///|
fn kv3_check_size(
  value : KV3Value,
  depth : Int,
  budget : Ref[Int],
  nodes : Ref[Int],
) -> Unit raise WriteError {
  nodes.val += 1
  if depth > 128 || nodes.val > 100000 {
    raise InvalidDocument("KV3 writer resource limit exceeded")
  }
  let mut size = 2 * (depth + 1) + 8
  match value {
    KV3String(s) | KV3Integer(s) | KV3Float(s) => {
      let chars = s.char_length()
      if chars > 1048576 {
        raise InvalidDocument("KV3 token exceeds limit")
      }
      size += chars * 2
    }
    KV3Blob(bytes) => {
      if bytes.length() > 1048576 {
        raise InvalidDocument("KV3 blob exceeds limit")
      }
      size += bytes.length() * 3
    }
    KV3Object(entries) =>
      for entry in entries {
        let chars = entry.key.char_length()
        if chars > 1048576 || chars * 2 + 4 > budget.val {
          raise InvalidDocument("KV3 output exceeds writer budget")
        }
        budget.val -= chars * 2 + 4
        kv3_check_size(entry.value, depth + 1, budget, nodes)
      }
    KV3Array(values) =>
      for child in values {
        kv3_check_size(child, depth + 1, budget, nodes)
      }
    KV3Flag(_, child) => {
      size += 20
      kv3_check_size(child, depth + 1, budget, nodes)
    }
    _ => ()
  }
  if size > budget.val {
    raise InvalidDocument("KV3 output exceeds writer budget")
  }
  budget.val -= size
}