// Runtime library for the MoonBit code generated by atdmbt.
//
// Readers have type `(Json, Path) -> T raise JsonError` and writers have
// type `(T) -> Json`. Combinators such as `read_list` build readers and
// writers for composite types from the readers and writers of their
// components.

///|
/// Location of a JSON value within the root JSON value, used in error
/// messages.
pub(all) enum Path {
  Root
  Key(Path, String)
  Index(Path, Int)
}

///|
/// The path of the field `key` of the object at this path.
pub fn Path::key(self : Path, key : String) -> Path {
  Key(self, key)
}

///|
/// The path of the element `i` of the array at this path.
pub fn Path::index(self : Path, i : Int) -> Path {
  Index(self, i)
}

///|
/// Format a path in the JSONPath syntax, e.g. `$.items[2].id`.
pub fn Path::to_string(self : Path) -> String {
  let buf = StringBuilder()
  fn go(p : Path) -> Unit {
    match p {
      Root => buf.write_string("$")
      Key(p, k) => {
        go(p)
        let simple = k != "" &&
          k
          .iter()
          .all(c => {
            (c >= 'a' && c <= 'z') ||
            (c >= 'A' && c <= 'Z') ||
            (c >= '0' && c <= '9') ||
            c == '_'
          })
        if simple {
          buf.write_string(".")
          buf.write_string(k)
        } else {
          buf.write_string("[")
          buf.write_string(Json::string(k).stringify())
          buf.write_string("]")
        }
      }
      Index(p, i) => {
        go(p)
        buf.write_string("[\{i}]")
      }
    }
  }

  go(self)
  buf.to_string()
}

///|
pub impl Show for Path with fn output(self, logger) {
  logger.write_string(self.to_string())
}

///|
/// Error raised when a JSON value doesn't have the expected shape.
pub(all) suberror JsonError {
  JsonError(String)
} derive(Eq, Debug)

///|
pub impl Show for JsonError with fn output(self, logger) {
  match self {
    JsonError(msg) => logger.write_string(msg)
  }
}

///|
/// The error message.
pub fn JsonError::message(self : JsonError) -> String {
  match self {
    JsonError(msg) => msg
  }
}

///|
/// A short representation of a JSON value for error messages.
fn excerpt(x : Json) -> String {
  let s = x.stringify()
  if s.length() > 200 {
    String::from_iter(s.iter().take(200)) + "…"
  } else {
    s
  }
}

///|
/// Fail with a message indicating the type that was expected.
pub fn[T] bad_json(
  expected : String,
  x : Json,
  path : Path,
) -> T raise JsonError {
  raise JsonError(
    "incompatible JSON value where type '\{expected}' was expected: '\{excerpt(x)}' at \{path}",
  )
}

///|
/// Fail because a required field is missing.
pub fn[T] missing_field(
  type_name : String,
  json_field_name : String,
  path : Path,
) -> T raise JsonError {
  raise JsonError(
    "missing field '\{json_field_name}' in JSON object of type '\{type_name}' at \{path}",
  )
}

// Parsing and printing

///|
/// Print JSON compactly, or with the given indentation.
pub fn stringify(x : Json, indent? : Int = 0) -> String {
  x.stringify(indent~)
}

// Readers for the predefined types

///|
/// Read `null` as `unit`.
pub fn read_unit(x : Json, path : Path) -> Unit raise JsonError {
  match x {
    Null => ()
    _ => bad_json("unit", x, path)
  }
}

///|
/// Read a boolean.
pub fn read_bool(x : Json, path : Path) -> Bool raise JsonError {
  match x {
    True => true
    False => false
    _ => bad_json("bool", x, path)
  }
}

///|
/// Whether the text of a JSON number is an integer literal (no fraction,
/// no exponent), like the integers accepted by atdgen.
fn is_integer_literal(r : String) -> Bool {
  r != "" && r.iter().all(c => (c >= '0' && c <= '9') || c == '-')
}

///|
/// Read an integer that must fit in 32 bits.
///
/// When the text of the number is known (see `parse`), it must be an
/// integer literal: `1.0` or `1e3` are rejected, like with atdgen.
/// Otherwise, the number must be an integral value within range.
pub fn read_int(x : Json, path : Path) -> Int raise JsonError {
  match x {
    Number(_, repr=Some(r)) if is_integer_literal(r) =>
      @string.parse_int(r) catch {
        _ => bad_json("int", x, path)
      }
    Number(n, repr=None) if n == n.floor() &&
      n >= -2147483648.0 &&
      n <= 2147483647.0 => n.to_int()
    _ => bad_json("int", x, path)
  }
}

///|
/// Read a 64-bit integer, using the exact text of the number when
/// available (see `read_int`).
pub fn read_int64(x : Json, path : Path) -> Int64 raise JsonError {
  match x {
    Number(_, repr=Some(r)) if is_integer_literal(r) =>
      @string.parse_int64(r) catch {
        _ => bad_json("int64", x, path)
      }
    Number(n, repr=None) if n == n.floor() &&
      n >= -9223372036854775808.0 &&
      n < 9223372036854775808.0 => n.to_int64()
    _ => bad_json("int64", x, path)
  }
}

///|
/// Read an integer represented as a JSON string (``).
pub fn read_int_of_string(x : Json, path : Path) -> Int raise JsonError {
  match x {
    String(s) => @string.parse_int(s) catch { _ => bad_json("int", x, path) }
    _ => bad_json("int", x, path)
  }
}

///|
/// Read a 64-bit integer represented as a JSON string.
pub fn read_int64_of_string(x : Json, path : Path) -> Int64 raise JsonError {
  match x {
    String(s) =>
      @string.parse_int64(s) catch {
        _ => bad_json("int64", x, path)
      }
    _ => bad_json("int64", x, path)
  }
}

///|
/// Read a number.
pub fn read_float(x : Json, path : Path) -> Double raise JsonError {
  match x {
    Number(n, ..) => n
    _ => bad_json("float", x, path)
  }
}

///|
/// Read a string.
pub fn read_string(x : Json, path : Path) -> String raise JsonError {
  match x {
    String(s) => s
    _ => bad_json("string", x, path)
  }
}

///|
/// Read any JSON value (ATD type `abstract`).
#warnings("-unused_error_type")
pub fn read_json(x : Json, _path : Path) -> Json raise JsonError {
  x
}

// Writers for the predefined types

///|
/// Write `unit` as `null`.
pub fn write_unit(_x : Unit) -> Json {
  Json::null()
}

///|
/// Write a boolean.
pub fn write_bool(x : Bool) -> Json {
  Json::boolean(x)
}

///|
/// Write an integer.
pub fn write_int(x : Int) -> Json {
  Json::number(x.to_double())
}

///|
/// Write a 64-bit integer, preserving all its digits.
pub fn write_int64(x : Int64) -> Json {
  Json::number(x.to_double(), repr=x.to_string())
}

///|
/// Write an integer as a JSON string (``).
pub fn write_int_to_string(x : Int) -> Json {
  Json::string(x.to_string())
}

///|
/// Write a 64-bit integer as a JSON string.
pub fn write_int64_to_string(x : Int64) -> Json {
  Json::string(x.to_string())
}

///|
/// Write a number.
pub fn write_float(x : Double) -> Json {
  Json::number(x)
}

///|
/// The smallest positive normal number, OCaml's `min_float`.
let min_normal_float : Double = 2.2250738585072014e-308

///|
/// Write a number rounded to an integer (``), exactly like
/// atdgen: positive numbers are rounded half up; other numbers are
/// formatted with C's `%.0f`, which rounds half to even (and may print
/// `-0`). Non-finite numbers can't be represented in JSON; they are written
/// as `NaN` or `Infinity`, which is not valid JSON (atdgen raises an error).
pub fn write_float_as_int(x : Double) -> Json {
  if x.is_nan() || x.is_inf() {
    return Json::number(x)
  }
  let text = if x >= min_normal_float {
    format_integral((x + 0.5).trunc())
  } else {
    format_float_fixed0(x)
  }
  let value = @string.parse_double(text) catch { _ => x }
  Json::number(value, repr=text)
}

///|
/// Build a writer for numbers with at most `precision` significant digits
/// (``), formatted like atdgen does (C's `%.Ng`).
pub fn write_float_prec(precision : Int) -> (Double) -> Json {
  x => {
    if x.is_nan() || x.is_inf() {
      Json::number(x)
    } else {
      // like Yojson, precisions outside of 1..16 mean 17
      let p = if precision >= 1 && precision <= 16 { precision } else { 17 }
      let s = format_float_g(x, p)
      let s = if s.iter().all(c => (c >= '0' && c <= '9') || c == '-') {
        s + ".0"
      } else {
        s
      }
      let v = @string.parse_double(s) catch { _ => x }
      Json::number(v, repr=s)
    }
  }
}

///|
/// Write a string.
pub fn write_string(x : String) -> Json {
  Json::string(x)
}

///|
/// Write any JSON value (ATD type `abstract`).
pub fn write_json(x : Json) -> Json {
  x
}

// Combinators

///|
/// Reader for `t list`.
pub fn[T] read_list(
  read_elt : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> Array[T] raise JsonError {
  (x, path) => {
    match x {
      Array(l) => {
        let res = []
        for i, v in l {
          res.push(read_elt(v, path.index(i)))
        }
        res
      }
      _ => bad_json("array", x, path)
    }
  }
}

///|
/// Writer for `t list`.
pub fn[T] write_list(write_elt : (T) -> Json) -> (Array[T]) -> Json {
  l => Json::array(l.map(write_elt))
}

///|
/// Reader for `t option`, encoded as `"None"` or `["Some", x]`.
pub fn[T] read_option(
  read_elt : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> T? raise JsonError {
  (x, path) => {
    match x {
      String("None") => None
      Array([String("Some"), v]) => Some(read_elt(v, path.index(1)))
      _ => bad_json("option", x, path)
    }
  }
}

///|
/// Writer for `t option`.
pub fn[T] write_option(write_elt : (T) -> Json) -> (T?) -> Json {
  x => {
    match x {
      None => Json::string("None")
      Some(v) => Json::array([Json::string("Some"), write_elt(v)])
    }
  }
}

///|
/// Reader for `t nullable`, encoded as `null` or the value itself.
pub fn[T] read_nullable(
  read_elt : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> T? raise JsonError {
  (x, path) => {
    match x {
      Null => None
      _ => Some(read_elt(x, path))
    }
  }
}

///|
/// Writer for `t nullable`.
pub fn[T] write_nullable(write_elt : (T) -> Json) -> (T?) -> Json {
  x => {
    match x {
      None => Json::null()
      Some(v) => write_elt(v)
    }
  }
}

///|
/// Check that a JSON value is an array of length `n` and return its
/// elements.
pub fn read_tuple(
  x : Json,
  path : Path,
  n : Int,
) -> Array[Json] raise JsonError {
  match x {
    Array(l) if l.length() == n => l
    _ => bad_json("tuple of length \{n}", x, path)
  }
}

///|
/// Reader for `(string * t) list ` represented as an
/// array of pairs.
pub fn[T] read_assoc_object_into_array(
  read_value : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> Array[(String, T)] raise JsonError {
  (x, path) => {
    match x {
      Object(o) => {
        let res = []
        for k, v in o {
          res.push((k, read_value(v, path.key(k))))
        }
        res
      }
      _ => bad_json("object", x, path)
    }
  }
}

///|
/// Writer for `(string * t) list ` represented as an
/// array of pairs.
pub fn[T] write_assoc_array_to_object(
  write_value : (T) -> Json,
) -> (Array[(String, T)]) -> Json {
  l => {
    let o : Map[String, Json] = Map([])
    for kv in l {
      o[kv.0] = write_value(kv.1)
    }
    Json::object(o)
  }
}

///|
/// Reader for `(string * t) list  `.
pub fn[T] read_assoc_object_into_map(
  read_value : (Json, Path) -> T raise JsonError,
) -> (Json, Path) -> Map[String, T] raise JsonError {
  (x, path) => {
    match x {
      Object(o) => {
        let res = Map([])
        for k, v in o {
          res[k] = read_value(v, path.key(k))
        }
        res
      }
      _ => bad_json("object", x, path)
    }
  }
}

///|
/// Writer for `(string * t) list  `.
pub fn[T] write_assoc_map_to_object(
  write_value : (T) -> Json,
) -> (Map[String, T]) -> Json {
  m => {
    let o : Map[String, Json] = Map([])
    for k, v in m {
      o[k] = write_value(v)
    }
    Json::object(o)
  }
}

///|
/// Reader for `(k * v) list `, encoded as a JSON array of
/// pairs.
pub fn[K : Hash + Eq, V] read_assoc_array_into_map(
  read_key : (Json, Path) -> K raise JsonError,
  read_value : (Json, Path) -> V raise JsonError,
) -> (Json, Path) -> Map[K, V] raise JsonError {
  (x, path) => {
    match x {
      Array(l) => {
        let res = Map([])
        for i, pair in l {
          let p = path.index(i)
          match pair {
            Array([k, v]) =>
              res[read_key(k, p.index(0))] = read_value(v, p.index(1))
            _ => bad_json("pair", pair, p)
          }
        }
        res
      }
      _ => bad_json("array", x, path)
    }
  }
}

///|
/// Writer for `(k * v) list `.
pub fn[K, V] write_assoc_map_to_array(
  write_key : (K) -> Json,
  write_value : (V) -> Json,
) -> (Map[K, V]) -> Json {
  m => {
    let res = []
    for k, v in m {
      res.push(Json::array([write_key(k), write_value(v)]))
    }
    Json::array(res)
  }
}

// Records

///|
/// Check that a JSON value is an object and return its fields.
pub fn read_object(
  x : Json,
  path : Path,
  type_name : String,
) -> Map[String, Json] raise JsonError {
  match x {
    Object(o) => o
    _ => bad_json(type_name, x, path)
  }
}

///|
/// Look up the field of an object, treating `null` like a missing field
/// unless `keep_nulls` is set.
pub fn get_field(
  o : Map[String, Json],
  key : String,
  keep_nulls? : Bool = false,
) -> Json? {
  match o.get(key) {
    Some(Null) if !keep_nulls => None
    x => x
  }
}

///|
/// Read a required field.
pub fn[T] read_required_field(
  o : Map[String, Json],
  type_name : String,
  key : String,
  read_value : (Json, Path) -> T raise JsonError,
  path : Path,
) -> T raise JsonError {
  match o.get(key) {
    Some(v) => read_value(v, path.key(key))
    None => missing_field(type_name, key, path)
  }
}

///|
/// Read an optional field, missing or `null` meaning `None`.
pub fn[T] read_optional_field(
  o : Map[String, Json],
  key : String,
  read_value : (Json, Path) -> T raise JsonError,
  path : Path,
  keep_nulls? : Bool = false,
) -> T? raise JsonError {
  match get_field(o, key, keep_nulls~) {
    Some(v) => Some(read_value(v, path.key(key)))
    None => None
  }
}

///|
/// If the JSON value is an object with a single field, return it.
/// This is used for reading sum types represented as JSON objects
/// (``).
pub fn read_single_field(x : Json) -> (String, Json)? {
  match x {
    Object(o) if o.length() == 1 =>
      for k, v in o {
        break Some((k, v))
      } nobreak {
        None
      }
    _ => None
  }
}