///|
/// Represents a JSON5 value tree.
///
/// This enum is the primary data model for parsed JSON5 content in this
/// package. It mirrors regular JSON shapes while preserving JSON5-friendly
/// numeric forms during parsing (for example `NaN` and `Infinity`).
///
/// Variants:
/// - `Null`
/// - `True`
/// - `False`
/// - `Number(Double)`
/// - `String(String)`
/// - `Array(Array[Json5Value])`
/// - `Object(Map[String, Json5Value])`
///
/// Example:
///
/// ```mbt check
/// test {
///   let value = @json5.parse("{foo: [1, true, 'x']}")
///   guard value is Object(obj)
///   assert_true(obj.get("foo") is Some(Array(_)))
/// }
/// ```
pub enum Json5Value {
  Null
  True
  False
  Number(Double)
  String(String)
  Array(Array[Json5Value])
  Object(Map[String, Json5Value])
} derive(Eq, Debug)

///|
/// Renders `Json5Value` as compact JSON5 text for display.
pub impl Show for Json5Value with output(self, logger) {
  logger.write_string(stringify(self))
}

///|
/// Converts `Json5Value` to core `Json`.
///
/// This is useful when you want to parse with JSON5 syntax and then operate
/// with MoonBit's standard `@json` ecosystem.
///
/// Notes:
/// - `NaN` and infinities are delegated to `Double::to_json` behavior.
/// - Object key order follows the underlying map iteration order.
pub impl ToJson for Json5Value with to_json(self : Json5Value) -> Json {
  match self {
    Null => Json::null()
    True => Json::boolean(true)
    False => Json::boolean(false)
    Number(n) => n.to_json()
    String(s) => Json::string(s)
    Array(arr) => Json::array(arr.map(v => v.to_json()))
    Object(obj) => {
      let out = Map::new()
      for k, v in obj {
        out.set(k, v.to_json())
      }
      Json::object(out)
    }
  }
}

///|
/// Converts core `Json` to `Json5Value`.
///
/// This implementation is recursive and propagates JSON path information to
/// nested conversions, making decode errors easier to locate.
///
/// Typical usage:
///
/// ```mbt check
/// test {
///   let json : Json = { "name": "moonbit", "ok": true }
///   let value : @json5.Json5Value = @json.from_json(json)
///   guard value is Object(obj)
///   assert_true(obj.get("name") is Some(_))
/// }
/// ```
pub impl @json.FromJson for Json5Value with from_json(json, path) {
  match json {
    Null => Null
    True => True
    False => False
    Number(n, ..) => Number(n)
    String(s) => String(s)
    Array(arr) =>
      Array(arr.mapi((i, v) => @json.from_json(v, path=path.add_index(i))))
    Object(obj) => {
      let out = Map::new()
      for k, v in obj {
        out.set(k, @json.from_json(v, path=path.add_key(k)))
      }
      Object(out)
    }
  }
}

///|
/// Internal token type produced by the lexer.
///
/// Kept private because token-level API is not intended for external use.
priv enum Token {
  LBrace
  RBrace
  LBracket
  RBracket
  Comma
  Colon
  String(String)
  Number(Double)
  True
  False
  Null
  EOF
} derive(Eq)

///|
impl Show for Token with output(self, logger) {
  match self {
    LBrace => logger.write_string("{")
    RBrace => logger.write_string("}")
    LBracket => logger.write_string("[")
    RBracket => logger.write_string("]")
    Comma => logger.write_string(",")
    Colon => logger.write_string(":")
    String(s) => {
      logger.write_char('"')
      logger.write_string(s)
      logger.write_char('"')
    }
    Number(n) => logger.write_string(n.to_string())
    True => logger.write_string("true")
    False => logger.write_string("false")
    Null => logger.write_string("null")
    EOF => logger.write_string("")
  }
}

///|
/// Internal lexer state for JSON5 tokenization.
///
/// The lexer supports JSON5-specific lexical features such as comments,
/// unquoted identifiers, single-quoted strings and hexadecimal numbers.
priv struct Lexer {
  input : String
  mut pos : Int
}

///|
fn Lexer::new(input : String) -> Lexer {
  { input, pos: 0 }
}

///|
fn Lexer::is_eof(self : Lexer) -> Bool {
  self.pos >= self.input.length()
}

///|
fn Lexer::peek(self : Lexer) -> Char {
  if self.is_eof() {
    '\u{0000}'
  } else {
    match self.input.code_unit_at(self.pos).to_char() {
      Some(c) => c
      None => '\u{0000}'
    }
  }
}

///|
fn Lexer::next(self : Lexer) -> Unit {
  self.pos = self.pos + 1
}

///|
/// Skips spaces and JSON5 comments.
///
/// Supported comments:
/// - Line comments: `// ...`
/// - Block comments: `/* ... */`
fn Lexer::skip_whitespace(self : Lexer) -> Unit {
  while !self.is_eof() {
    let c = self.peek()
    if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
      self.next()
      continue
    }
    if c == '/' {
      self.next()
      if self.is_eof() {
        return
      }
      let next = self.peek()
      if next == '/' {
        self.next()
        while !self.is_eof() {
          let p = self.peek()
          if p == '\n' || p == '\r' {
            break
          }
          self.next()
        }
        continue
      }
      if next == '*' {
        self.next()
        while !self.is_eof() {
          let p = self.peek()
          if p == '*' {
            self.next()
            if !self.is_eof() && self.peek() == '/' {
              self.next()
              break
            }
          } else {
            self.next()
          }
        }
        continue
      }
      self.pos = self.pos - 1
      break
    }
    break
  }
}

///|
/// Reads the next token from input.
///
/// Raises an error for unsupported or malformed characters.
fn Lexer::next_token(self : Lexer) -> Token raise Error {
  self.skip_whitespace()
  if self.is_eof() {
    return EOF
  }
  let c = self.peek()
  match c {
    '{' => {
      self.next()
      LBrace
    }
    '}' => {
      self.next()
      RBrace
    }
    '[' => {
      self.next()
      LBracket
    }
    ']' => {
      self.next()
      RBracket
    }
    ',' => {
      self.next()
      Comma
    }
    ':' => {
      self.next()
      Colon
    }
    '\"' | '\'' => self.read_string()
    _ =>
      if is_digit(c) || c == '-' || c == '+' || c == '.' {
        self.read_number()
      } else if is_alpha(c) {
        self.read_identifier()
      } else {
        fail("unexpected character: \{c}")
      }
  }
}

///|
fn is_digit(c : Char) -> Bool {
  c >= '0' && c <= '9'
}

///|
fn is_alpha(c : Char) -> Bool {
  (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$'
}

///|
fn is_identifier_continue(c : Char) -> Bool {
  is_alpha(c) || is_digit(c)
}

///|
fn hex_value(c : Char) -> Int raise Error {
  if c >= '0' && c <= '9' {
    c.to_int() - '0'.to_int()
  } else if c >= 'a' && c <= 'f' {
    10 + c.to_int() - 'a'.to_int()
  } else if c >= 'A' && c <= 'F' {
    10 + c.to_int() - 'A'.to_int()
  } else {
    fail("invalid hex digit: \{c}")
  }
}

///|
fn parse_hex_number(s : String) -> Double raise Error {
  if s.length() <= 2 {
    fail("invalid hex number: \{s}")
  }
  let mut value = 0
  for i in 2.. c
      None => fail("invalid UTF-16 code unit")
    }
    value = value * 16 + hex_value(c)
  }
  value.to_double()
}

///|
/// Reads a quoted JSON5 string token.
///
/// Supported escapes include `\n`, `\r`, `\t`, `\\`, `\"`, and `\'`.
/// Unicode escape `\uXXXX` is currently not implemented and will raise.
fn Lexer::read_string(self : Lexer) -> Token raise Error {
  let quote = self.peek()
  self.next()
  let mut s = ""
  while !self.is_eof() {
    let c = self.peek()
    if c == quote {
      self.next()
      return String(s)
    }
    if c == '\\' {
      self.next()
      if self.is_eof() {
        fail("unexpected EOF in string")
      }
      let esc = self.peek()
      match esc {
        '\"' => s = s + "\""
        '\'' => s = s + "'"
        '\\' => s = s + "\\"
        '/' => s = s + "/"
        'b' => s = s + "\b"
        'f' => s = s + "\u000C"
        'n' => s = s + "\n"
        'r' => s = s + "\r"
        't' => s = s + "\t"
        'u' => fail("unicode escape is not supported yet")
        _ => s = s + esc.to_string()
      }
      self.next()
    } else {
      if c == '\n' || c == '\r' {
        fail("unexpected newline in string")
      }
      s = s + c.to_string()
      self.next()
    }
  }
  fail("unexpected EOF in string")
}

///|
/// Reads a JSON5 number token.
///
/// Supported forms include:
/// - Decimal numbers
/// - Signed numbers
/// - Exponent notation
/// - Hex notation: `0x...`
/// - `Infinity`, `-Infinity`, `NaN`
fn Lexer::read_number(self : Lexer) -> Token raise Error {
  let start = self.pos
  while !self.is_eof() {
    let c = self.peek()
    if is_digit(c) ||
      c == '.' ||
      c == '-' ||
      c == '+' ||
      c == 'e' ||
      c == 'E' ||
      c == 'x' ||
      c == 'X' ||
      (c >= 'a' && c <= 'f') ||
      (c >= 'A' && c <= 'F') {
      self.next()
    } else {
      break
    }
  }
  let s = self.input[start:self.pos].to_owned()
  if s == "Infinity" || s == "+Infinity" {
    return Number(1.0 / 0.0)
  }
  if s == "-Infinity" {
    return Number(-1.0 / 0.0)
  }
  if s == "NaN" {
    return Number(0.0 / 0.0)
  }
  if s.has_prefix("0x") || s.has_prefix("0X") {
    return Number(parse_hex_number(s))
  }
  Number(@string.parse_double(s))
}

///|
/// Reads JSON5 identifiers and reserved words.
///
/// Reserved words are converted to dedicated tokens (`true`, `false`, `null`,
/// `Infinity`, `NaN`). Other identifiers are emitted as string tokens, which
/// are used by the parser for unquoted object keys.
fn Lexer::read_identifier(self : Lexer) -> Token {
  let start = self.pos
  while !self.is_eof() && is_identifier_continue(self.peek()) {
    self.next()
  }
  let s = self.input[start:self.pos].to_owned()
  match s {
    "true" => True
    "false" => False
    "null" => Null
    "Infinity" => Number(1.0 / 0.0)
    "NaN" => Number(0.0 / 0.0)
    _ => String(s) // For unquoted keys
  }
}

///|
/// Internal recursive-descent parser for JSON5 values.
priv struct Parser {
  lexer : Lexer
  mut current_token : Token
}

///|
fn Parser::new(input : String) -> Parser raise Error {
  let lexer = Lexer::new(input)
  let current_token = lexer.next_token()
  { lexer, current_token }
}

///|
fn Parser::advance(self : Parser) -> Unit raise Error {
  self.current_token = self.lexer.next_token()
}

///|
/// Parses any JSON5 value from the current token stream position.
fn Parser::parse_value(self : Parser) -> Json5Value raise Error {
  match self.current_token {
    Null => {
      self.advance()
      Null
    }
    True => {
      self.advance()
      True
    }
    False => {
      self.advance()
      False
    }
    Number(n) => {
      self.advance()
      Number(n)
    }
    String(s) => {
      self.advance()
      String(s)
    }
    LBrace => self.parse_object()
    LBracket => self.parse_array()
    _ => fail("unexpected token: \{self.current_token}")
  }
}

///|
/// Parses a JSON5 object.
///
/// Supports:
/// - quoted and unquoted keys
/// - trailing comma
fn Parser::parse_object(self : Parser) -> Json5Value raise Error {
  self.advance() // skip {
  let obj = Map::new()
  if self.current_token == RBrace {
    self.advance()
    return Object(obj)
  }
  while self.current_token != RBrace {
    let key = match self.current_token {
      String(s) => s
      _ => fail("expected object key, got \{self.current_token}")
    }
    self.advance() // skip key
    if self.current_token != Colon {
      fail("expected ':', got \{self.current_token}")
    }
    self.advance() // skip :
    let val = self.parse_value()
    obj.set(key, val)
    if self.current_token == Comma {
      self.advance()
      if self.current_token == RBrace {
        break
      }
      continue
    } else if self.current_token == RBrace {
      break
    } else {
      fail("expected ',' or '}', got \{self.current_token}")
    }
  }
  self.advance() // skip }
  Object(obj)
}

///|
/// Parses a JSON5 array.
///
/// Supports trailing comma.
fn Parser::parse_array(self : Parser) -> Json5Value raise Error {
  self.advance() // skip [
  let arr = []
  if self.current_token == RBracket {
    self.advance()
    return Array(arr)
  }
  while self.current_token != RBracket {
    let val = self.parse_value()
    arr.push(val)
    if self.current_token == Comma {
      self.advance()
      if self.current_token == RBracket {
        break
      }
      continue
    } else if self.current_token == RBracket {
      break
    } else {
      fail("expected ',' or ']', got \{self.current_token}")
    }
  }
  self.advance() // skip ]
  Array(arr)
}

///|
/// Parses a JSON5 text into `Json5Value`.
///
/// This is the primary entry point for JSON5 parsing in this package.
///
/// Raises `Error` for malformed input.
///
/// Example:
///
/// ```mbt check
/// test {
///   let value = @json5.parse("{foo: 'bar', nums: [1, 2,],}")
///   guard value is Object(obj)
///   assert_true(obj.get("foo") is Some(_))
/// }
/// ```
pub fn parse(input : String) -> Json5Value raise Error {
  let p = Parser::new(input)
  let val = p.parse_value()
  if p.current_token != EOF {
    fail("unexpected token at EOF: \{p.current_token}")
  }
  val
}

///|
/// Parses JSON5 text directly into core `Json`.
///
/// This helper is convenient when your downstream code already uses `@json`
/// APIs and you do not need to manually inspect `Json5Value`.
///
/// Equivalent to:
///
/// `parse(input).to_json()`
pub fn parse_json(input : String) -> Json raise Error {
  parse(input).to_json()
}

///|
/// Escapes a string for JSON/JSON5 output.
///
/// Handles common control characters and quote/backslash escaping.
fn escape_string(s : String) -> String {
  let mut out = ""
  for c in s {
    match c {
      '\\' => out = out + "\\\\"
      '\"' => out = out + "\\\""
      '\n' => out = out + "\\n"
      '\r' => out = out + "\\r"
      '\t' => out = out + "\\t"
      _ => out = out + c.to_string()
    }
  }
  out
}

///|
/// Returns true if the string is a valid JSON5 unquoted identifier key.
///
/// A valid identifier starts with a letter, `$`, or `_`, followed by
/// letters, digits, `$`, or `_`.
fn is_json5_identifier(s : String) -> Bool {
  if s.is_empty() {
    return false
  }
  let mut i = 0
  for c in s {
    let ok = if i == 0 {
      c == '_' || c == '$' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
    } else {
      c == '_' ||
      c == '$' ||
      (c >= 'a' && c <= 'z') ||
      (c >= 'A' && c <= 'Z') ||
      (c >= '0' && c <= '9')
    }
    if !ok {
      return false
    }
    i = i + 1
  }
  true
}

///|
/// Serializes `Json5Value` to compact text.
///
/// Object keys that are valid JSON5 identifiers are written unquoted;
/// all other keys are quoted.
///
/// Number behavior:
/// - finite numbers: normal decimal formatting
/// - `NaN`: `"NaN"`
/// - positive infinity: `"Infinity"`
/// - negative infinity: `"-Infinity"`
///
/// Example:
///
/// ```mbt check
/// test {
///   let value = @json5.parse("{foo: 'bar'}")
///   let text = @json5.stringify(value)
///   assert_true(text.contains("foo:\"bar\""))
/// }
/// ```
pub fn stringify(val : Json5Value) -> String {
  match val {
    Null => "null"
    True => "true"
    False => "false"
    Number(n) =>
      if n.is_nan() {
        "NaN"
      } else if n == 1.0 / 0.0 {
        "Infinity"
      } else if n == -1.0 / 0.0 {
        "-Infinity"
      } else {
        n.to_string()
      }
    String(s) => "\"" + escape_string(s) + "\""
    Array(arr) => {
      let mut s = "["
      for i, v in arr {
        if i > 0 {
          s = s + ","
        }
        s = s + stringify(v)
      }
      s = s + "]"
      s
    }
    Object(obj) => {
      let mut s = "{"
      let mut first = true
      for k, v in obj {
        if !first {
          s = s + ","
        }
        let key = if is_json5_identifier(k) {
          k
        } else {
          "\"" + escape_string(k) + "\""
        }
        s = s + key + ":" + stringify(v)
        first = false
      }
      s = s + "}"
      s
    }
  }
}

///|
/// Writes a `Json5Value` to a `StringBuilder` with pretty indentation.
///
/// Object keys that are valid JSON5 identifiers are written unquoted.
fn stringify_pretty_write(
  buf : StringBuilder,
  val : Json5Value,
  indent : Int,
  indent_size : Int,
) -> Unit {
  match val {
    Null => buf.write_string("null")
    True => buf.write_string("true")
    False => buf.write_string("false")
    Number(n) =>
      if n.is_nan() {
        buf.write_string("NaN")
      } else if n == 1.0 / 0.0 {
        buf.write_string("Infinity")
      } else if n == -1.0 / 0.0 {
        buf.write_string("-Infinity")
      } else {
        buf.write_string(n.to_string())
      }
    String(s) => {
      buf.write_char('"')
      buf.write_string(escape_string(s))
      buf.write_char('"')
    }
    Array(arr) =>
      if arr.is_empty() {
        buf.write_string("[]")
      } else {
        buf.write_string("[\n")
        let child = indent + indent_size
        for i, item in arr {
          for _ in 0..
      if obj.is_empty() {
        buf.write_string("{}")
      } else {
        buf.write_string("{\n")
        let child = indent + indent_size
        let entries = obj.iter().collect()
        for i, pair in entries {
          let (k, v) = pair
          for _ in 0.. String {
  let buf = StringBuilder::new()
  stringify_pretty_write(buf, val, 0, indent_size)
  buf.write_char('\n')
  buf.to_string()
}

///|
/// Directly converts core `Json` to `Json5Value` without going through the
/// `@json.FromJson` trait (which carries an unused error type in its signature).
fn json_to_json5(json : Json) -> Json5Value {
  match json {
    Null => Null
    True => True
    False => False
    Number(n, ..) => Number(n)
    String(s) => String(s)
    Array(arr) => Array(arr.map(json_to_json5))
    Object(obj) => {
      let out = Map::new()
      for k, v in obj {
        out.set(k, json_to_json5(v))
      }
      Object(out)
    }
  }
}

///|
/// Serializes core `Json` to a pretty-printed JSON5 string.
///
/// Convenience wrapper around `stringify_pretty` that converts `Json` first.
/// The `indent_size` labeled argument defaults to `2`.
pub fn stringify_json_pretty(json : Json, indent_size? : Int = 2) -> String {
  stringify_pretty(json_to_json5(json), indent_size~)
}

///|
/// Serializes core `Json` to compact JSON5 text.
///
/// Object keys that are valid JSON5 identifiers are written unquoted.
pub fn stringify_json(json : Json) -> String {
  stringify(json_to_json5(json))
}