///|
/// Locations use zero-based UTF-16 offsets and one-based lines and columns.
#derive(Eq, Debug)
pub(all) struct Position {
  offset : Int32
  line : Int32
  column : Int32
}

///|
#derive(Eq, Debug)
pub(all) enum DiagnosticKind {
  InvalidToml
  UnsupportedDottedKey
  UnsupportedArrayOfTables
  MissingPath
  WrongKind
  InvalidIndex
}

///|
#derive(Debug)
pub(all) suberror Diagnostic {
  Diagnostic(DiagnosticKind, String, Position)
}

///|
priv struct Span {
  start : Int32
  end : Int32
}

///|
/// Source is retained once. Every concrete node references its original span;
/// comments and whitespace remain in the gaps between nodes.
priv enum Value {
  Atom(Span)
  Sequence(Span, Array[Value], Array[Int32])
  Inline(Span, Array[Entry], Array[Int32])
}

///|
priv struct Entry {
  keys : Array[String]
  start : Int32
  value : Value
}

///|
priv enum TableKind {
  Standard
  ArrayOfTables
}

///|
priv enum Statement {
  Assignment(Array[String], Entry)
  Header(Array[String], TableKind, Span)
}

///|
/// Documents are immutable. Editing reparses a candidate before returning it.
pub struct Document {
  priv source : String
  priv statements : Array[Statement]
  priv focus : EditFocus?
}

///|
fn Value::span(self : Value) -> Span {
  match self {
    Atom(s) | Sequence(s, _, _) | Inline(s, _, _) => s
  }
}

///|
fn position(source : String, offset : Int32) -> Position {
  let mut line = 1
  let mut column = 1
  for i = 0; i < offset && i < source.length(); i = i + 1 {
    if source.code_unit_at(i).to_int() == 10 {
      line += 1
      column = 1
    } else {
      column += 1
    }
  }
  { offset, line, column, }
}

///|
fn problem(
  source : String,
  offset : Int32,
  kind : DiagnosticKind,
  message : String,
) -> Diagnostic {
  Diagnostic(kind, message, position(source, offset))
}

///|
priv struct Parser {
  source : String
  mut pos : Int32
}

///|
fn Parser::peek(self : Parser, ahead~ : Int32 = 0) -> Int32 {
  let i = self.pos + ahead
  if i < self.source.length() {
    self.source.code_unit_at(i).to_int()
  } else {
    -1
  }
}

///|
fn Parser::bad(self : Parser, message : String) -> Unit raise Diagnostic {
  raise problem(self.source, self.pos, InvalidToml, message)
}

///|
fn Parser::expect(self : Parser, c : Int32) -> Unit raise Diagnostic {
  if self.peek() != c {
    self.bad("Expected '" + c.unsafe_to_char().to_string() + "'")
  }
  self.pos += 1
}

///|
fn Parser::spaces(self : Parser) -> Unit {
  while self.peek() == 32 || self.peek() == 9 {
    self.pos += 1
  }
}

///|
fn Parser::newline(self : Parser) -> Unit raise Diagnostic {
  if self.peek() == 13 {
    self.pos += 1
  }
  self.expect(10)
}

///|
fn Parser::comment(self : Parser) -> Unit raise Diagnostic {
  self.expect(35)
  while self.peek() != -1 && self.peek() != 10 && self.peek() != 13 {
    let c = self.peek()
    if (c < 32 && c != 9) || c == 127 {
      self.bad("Control character in comment")
    }
    self.pos += 1
  }
}

///|
fn Parser::trivia(self : Parser) -> Unit raise Diagnostic {
  while true {
    self.spaces()
    match self.peek() {
      35 => self.comment()
      10 | 13 => self.newline()
      _ => break
    }
  }
}

///|
fn hex(c : Int32) -> Int32 {
  if c >= 48 && c <= 57 {
    c - 48
  } else if c >= 65 && c <= 70 {
    c - 55
  } else if c >= 97 && c <= 102 {
    c - 87
  } else {
    -1
  }
}

///|
/// Parse and decode a key or string, while the CST keeps its original spelling.
fn Parser::quoted(
  self : Parser,
  key~ : Bool = false,
) -> String raise Diagnostic {
  let quote = self.peek()
  self.pos += 1
  let multi = !key && self.peek() == quote && self.peek(ahead=1) == quote
  if multi {
    self.pos += 2
    if self.peek() == 10 || self.peek() == 13 {
      self.newline()
    }
  }
  let out = StringBuilder()
  while true {
    let c = self.peek()
    if c == -1 {
      self.bad("Unterminated string")
    }
    if c == quote {
      if !multi {
        self.pos += 1
        return out.to_string()
      }
      let mut n = 0
      while self.peek(ahead=n) == quote {
        n += 1
      }
      if n >= 3 {
        if n > 5 {
          self.bad("Too many closing quotes")
        }
        for i = 3; i < n; i = i + 1 {
          out.write_char(quote.unsafe_to_char())
        }
        self.pos += n
        return out.to_string()
      }
    }
    if c == 92 && quote == 34 {
      self.pos += 1
      let escape = self.peek()
      if multi && (escape == 32 || escape == 9 || escape == 10 || escape == 13) {
        self.spaces()
        self.newline()
        while self.peek() == 32 ||
              self.peek() == 9 ||
              self.peek() == 10 ||
              self.peek() == 13 {
          if self.peek() == 10 || self.peek() == 13 {
            self.newline()
          } else {
            self.pos += 1
          }
        }
        continue
      }
      if escape == -1 {
        self.bad("Unterminated escape")
      }
      self.pos += 1
      match escape {
        98 => out.write_char('\b')
        116 => out.write_char('\t')
        110 => out.write_char('\n')
        102 => out.write_char('\u{000c}')
        114 => out.write_char('\r')
        34 => out.write_char('"')
        92 => out.write_char('\\')
        117 | 85 => {
          let n = if escape == 117 { 4 } else { 8 }
          let mut scalar : Int64 = 0
          for i = 0; i < n; i = i + 1 {
            let h = hex(self.peek())
            if h < 0 {
              self.bad("Invalid Unicode escape")
            }
            scalar = scalar * 16 + h.to_int64()
            self.pos += 1
          }
          if scalar > 0x10ffff || (scalar >= 0xd800 && scalar <= 0xdfff) {
            self.bad("Unicode escape is not a scalar value")
          }
          out.write_char(scalar.to_int().unsafe_to_char())
        }
        _ => self.bad("Invalid string escape")
      }
    } else if multi && (c == 10 || c == 13) {
      self.newline()
      out.write_char('\n')
    } else {
      if (c < 32 && c != 9) || c == 127 {
        self.bad("Control character in string")
      }
      // Copy a complete Unicode scalar, including astral characters.
      if c >= 0xd800 && c <= 0xdbff {
        let low = self.peek(ahead=1)
        if low < 0xdc00 || low > 0xdfff {
          self.bad("Unpaired Unicode surrogate")
        }
        out.write_char(
          (0x10000 + (c - 0xd800) * 1024 + low - 0xdc00).unsafe_to_char(),
        )
        self.pos += 2
      } else {
        if c >= 0xdc00 && c <= 0xdfff {
          self.bad("Unpaired Unicode surrogate")
        }
        out.write_char(c.unsafe_to_char())
        self.pos += 1
      }
    }
  } nobreak {
    abort("unreachable")
  }
}

///|
fn bare(c : Int32) -> Bool {
  (c >= 65 && c <= 90) ||
  (c >= 97 && c <= 122) ||
  (c >= 48 && c <= 57) ||
  c == 95 ||
  c == 45
}

///|
fn Parser::keys(self : Parser) -> Array[String] raise Diagnostic {
  let keys = []
  while true {
    self.spaces()
    let start = self.pos
    let key = if self.peek() == 34 || self.peek() == 39 {
      self.quoted(key=true)
    } else {
      while bare(self.peek()) {
        self.pos += 1
      }
      if start == self.pos {
        self.bad("Expected a key")
      }
      self.source[start:self.pos].to_owned()
    }
    keys.push(key)
    self.spaces()
    if self.peek() != 46 {
      break
    }
    self.pos += 1
  }
  keys
}

///|
fn Parser::entry(self : Parser) -> Entry raise Diagnostic {
  let start = self.pos
  let keys = self.keys()
  self.expect(61)
  self.spaces()
  let value = self.value()
  { keys, start, value, }
}

///|
fn Parser::value(self : Parser) -> Value raise Diagnostic {
  let start = self.pos
  match self.peek() {
    34 | 39 => {
      ignore(self.quoted())
      Atom({ start, end: self.pos, })
    }
    91 => {
      self.pos += 1
      let values = []
      let commas = []
      self.trivia()
      while self.peek() != 93 {
        values.push(self.value())
        self.trivia()
        if self.peek() != 44 {
          break
        }
        commas.push(self.pos)
        self.pos += 1
        self.trivia()
      }
      self.expect(93)
      Sequence({ start, end: self.pos, }, values, commas)
    }
    123 => {
      self.pos += 1
      let entries = []
      let commas = []
      self.spaces()
      while self.peek() != 125 {
        entries.push(self.entry())
        self.spaces()
        if self.peek() != 44 {
          break
        }
        commas.push(self.pos)
        self.pos += 1
        self.spaces()
        if self.peek() == 125 {
          self.bad("Trailing comma in inline table")
        }
      }
      self.expect(125)
      validate_inline(self.source, entries)
      Inline({ start, end: self.pos, }, entries, commas)
    }
    _ => {
      while true {
        let c = self.peek()
        if c == -1 ||
          c == 9 ||
          c == 10 ||
          c == 13 ||
          c == 35 ||
          c == 44 ||
          c == 93 ||
          c == 125 {
          break
        }
        if c == 32 {
          // RFC 3339 permits a single space between date and time.
          if self.pos - start == 10 &&
            self.peek(ahead=1) >= 48 &&
            self.peek(ahead=1) <= 57 {
            self.pos += 1
            continue
          }
          break
        }
        self.pos += 1
      }
      let token = self.source[start:self.pos].to_owned()
      if !valid_atom(token) {
        self.bad("Invalid TOML value: " + token)
      }
      Atom({ start, end: self.pos, })
    }
  }
}

///|
pub fn parse(source : String) -> Document raise Diagnostic {
  // String callers can supply unpaired UTF-16 surrogates on the JS target.
  let mut i = 0
  while i < source.length() {
    let c = source.code_unit_at(i).to_int()
    if c >= 0xd800 && c <= 0xdbff {
      if i + 1 >= source.length() ||
        source.code_unit_at(i + 1).to_int() < 0xdc00 ||
        source.code_unit_at(i + 1).to_int() > 0xdfff {
        raise problem(source, i, InvalidToml, "Unpaired Unicode surrogate")
      }
      i += 2
    } else {
      if c >= 0xdc00 && c <= 0xdfff {
        raise problem(source, i, InvalidToml, "Unpaired Unicode surrogate")
      }
      i += 1
    }
  }
  let p = Parser::{ source, pos: 0, }
  let statements = []
  let mut table : Array[String] = []
  p.trivia()
  while p.peek() != -1 {
    if p.peek() == 91 {
      let start = p.pos
      p.pos += 1
      let aot = p.peek() == 91
      if aot {
        p.pos += 1
      }
      table = p.keys()
      p.expect(93)
      if aot {
        p.expect(93)
      }
      statements.push(
        Header(table, if aot { ArrayOfTables } else { Standard }, {
          start,
          end: p.pos,
        }),
      )
    } else {
      statements.push(Assignment(table, p.entry()))
    }
    p.spaces()
    if p.peek() == 35 {
      p.comment()
    }
    if p.peek() != -1 {
      p.newline()
    }
    p.trivia()
  }
  validate_document(source, statements)
  { source, statements, focus: None, }
}

///|
pub fn Document::encode(self : Document) -> String {
  self.source
}

///|
/// Strict UTF-8 boundary. No lossy replacement or BOM stripping is performed.
pub fn parse_bytes(bytes : Bytes) -> Document raise Diagnostic {
  let source = @utf8.decode(bytes, ignore_bom=false) catch {
    @utf8.Malformed(rest) => {
      let byte_offset = bytes.length() - rest.length()
      let preceding = @utf8.decode_lossy(bytes[:byte_offset], ignore_bom=false)
      raise problem(
        preceding,
        preceding.length(),
        InvalidToml,
        "Invalid UTF-8 at byte offset \{byte_offset}",
      )
    }
  }
  parse(source)
}

///|
/// Re-encode the unchanged source or the result of localized edits as UTF-8.
pub fn Document::encode_bytes(self : Document) -> Bytes {
  @utf8.encode(self.source)
}

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

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

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

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

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