// JSON helpers shared by the API groups. The core `Json` enum is used
// directly (Object/Array/String/Number/True/False/Null) and each type has a
// plain `*_of(Json)` / `*_to_json` pair in its own file. Plain functions, not
// trait impls, keep the package warning-clean under `--deny-warn`; the codecs
// are still "core `Json`", which is what the organisation's convention asks
// for.

///|
/// The JSON `null`, built through `Unit`'s `ToJson` (the `Null` variant is
/// read-only and cannot be constructed directly).
pub fn json_null() -> Json {
  @json.to_json(())
}

///|
/// The object map behind a `Json`, or an empty map when it is not an object.
pub fn json_map(j : Json) -> Map[String, Json] {
  match j {
    Object(m) => m
    _ => Map([])
  }
}

///|
/// The array behind a `Json`, or an empty array when it is not an array.
pub fn json_array(j : Json) -> Array[Json] {
  match j {
    Array(a) => a
    _ => []
  }
}

///|
/// A `Json` as a bool: `true` only for `True`.
pub fn json_bool(j : Json) -> Bool {
  match j {
    True => true
    _ => false
  }
}

///|
/// The `key` value as a string, or `""` when absent or not a string.
pub fn jstr(m : Map[String, Json], key : String) -> String {
  match m.get(key) {
    Some(String(s)) => s
    _ => ""
  }
}

///|
/// The `key` value as a string, or `None`.
pub fn jstr_opt(m : Map[String, Json], key : String) -> String? {
  match m.get(key) {
    Some(String(s)) => Some(s)
    _ => None
  }
}

///|
/// The `key` value as an `Int32`, or `0` when absent or not a number.
pub fn ji32(m : Map[String, Json], key : String) -> Int32 {
  match m.get(key) {
    Some(Number(n, ..)) => n.to_int()
    _ => 0
  }
}

///|
/// The `key` value as an `Int32`, or `None`.
pub fn ji32_opt(m : Map[String, Json], key : String) -> Int32? {
  match m.get(key) {
    Some(Number(n, ..)) => Some(n.to_int())
    _ => None
  }
}

///|
/// The `key` value as an `Int64`, or `0L` when absent or not a number.
pub fn ji64(m : Map[String, Json], key : String) -> Int64 {
  match m.get(key) {
    Some(Number(n, ..)) => n.to_int64()
    _ => 0L
  }
}

///|
/// The `key` value as an `Int64`, or `None`.
pub fn ji64_opt(m : Map[String, Json], key : String) -> Int64? {
  match m.get(key) {
    Some(Number(n, ..)) => Some(n.to_int64())
    _ => None
  }
}

///|
/// The `key` value as a `UInt64`, or `0`. A `Double` cannot hold a large one, so
/// it comes from the number's text.
pub fn ju64(m : Map[String, Json], key : String) -> UInt64 {
  ju64_opt(m, key).unwrap_or(0UL)
}

///|
/// The `key` value as a `UInt64`, or `None`.
pub fn ju64_opt(m : Map[String, Json], key : String) -> UInt64? {
  match m.get(key) {
    Some(Number(_, repr=Some(text))) => Some(parse_u64(text))
    Some(Number(n, ..)) => Some(n.to_uint64())
    _ => None
  }
}

///|
/// The `key` value as a `Double`, or `None`.
pub fn jdouble_opt(m : Map[String, Json], key : String) -> Double? {
  match m.get(key) {
    Some(Number(n, ..)) => Some(n)
    _ => None
  }
}

///|
/// The `key` value as a bool, or `None`.
pub fn jbool_opt(m : Map[String, Json], key : String) -> Bool? {
  match m.get(key) {
    Some(True) => Some(true)
    Some(False) => Some(false)
    _ => None
  }
}

///|
/// The `key` value as an array, or `[]` when absent or not an array.
pub fn jarray(m : Map[String, Json], key : String) -> Array[Json] {
  match m.get(key) {
    Some(Array(a)) => a
    _ => []
  }
}

///|
/// The `key` value as a string array, or `None`.
pub fn jstr_array_opt(m : Map[String, Json], key : String) -> Array[String]? {
  match m.get(key) {
    Some(Array(a)) => {
      let out : Array[String] = []
      for item in a {
        if item is String(s) {
          out.push(s)
        }
      }
      Some(out)
    }
    _ => None
  }
}

///|
/// The `key` value as a `String` map, or `None`.
pub fn jmap_opt(m : Map[String, Json], key : String) -> Map[String, String]? {
  match m.get(key) {
    Some(Object(o)) => {
      let out : Map[String, String] = Map([])
      for k, v in o {
        if v is String(s) {
          out[k] = s
        }
      }
      Some(out)
    }
    _ => None
  }
}

///|
/// The `key` value as bytes, base64-decoded (Consul's encoding for binary
/// fields), or `None`.
pub fn jbytes_b64_opt(m : Map[String, Json], key : String) -> Bytes? {
  match m.get(key) {
    Some(String(s)) =>
      if s == "" {
        Some(b"")
      } else {
        Some(@base64.decode_lossy(s))
      }
    _ => None
  }
}

///|
/// An empty JSON object to fill with `put_*`.
pub fn jobj() -> Map[String, Json] {
  Map([])
}

///|
/// A `String` map as a JSON object.
pub fn map_to_json(m : Map[String, String]) -> Json {
  let out : Map[String, Json] = Map([])
  for k, v in m {
    out[k] = @json.to_json(v)
  }
  @json.to_json(out)
}

///|
/// Set `m[key]` when `v` is `Some`, leaving it out when `None`.
pub fn put_str(m : Map[String, Json], key : String, v : String?) -> Unit {
  match v {
    Some(s) => m[key] = @json.to_json(s)
    None => ()
  }
}

///|
/// Set an `Int32` field when `Some`.
pub fn put_i32(m : Map[String, Json], key : String, v : Int32?) -> Unit {
  match v {
    Some(n) => m[key] = @json.to_json(n)
    None => ()
  }
}

///|
/// Set an `Int64` field when `Some`.
pub fn put_i64(m : Map[String, Json], key : String, v : Int64?) -> Unit {
  match v {
    Some(n) => m[key] = Json::number(n.to_double(), repr=n.to_string())
    None => ()
  }
}

///|
/// Set a `UInt64` field when `Some`, with its text so that a large one survives.
pub fn put_u64(m : Map[String, Json], key : String, v : UInt64?) -> Unit {
  match v {
    Some(n) => m[key] = Json::number(n.to_double(), repr=n.to_string())
    None => ()
  }
}

///|
/// Set a bool field when `Some`.
pub fn put_bool(m : Map[String, Json], key : String, v : Bool?) -> Unit {
  match v {
    Some(b) => m[key] = @json.to_json(b)
    None => ()
  }
}

///|
/// Set a nested JSON value when `Some`.
pub fn put_json(m : Map[String, Json], key : String, v : Json?) -> Unit {
  match v {
    Some(j) => m[key] = j
    None => ()
  }
}

///|
/// Set a string-array field when `Some`.
pub fn put_str_array(
  m : Map[String, Json],
  key : String,
  v : Array[String]?,
) -> Unit {
  match v {
    Some(items) => {
      let arr : Array[Json] = []
      for item in items {
        arr.push(@json.to_json(item))
      }
      m[key] = @json.to_json(arr)
    }
    None => ()
  }
}

///|
/// Parse a decimal `Int64` from a string, returning `0L` on anything non-numeric.
pub fn parse_u64(s : StringView) -> UInt64 {
  let mut n = 0UL
  for ch in s {
    let code = ch.to_int()
    if code < 48 || code > 57 {
      break
    }
    n = n * 10UL + (code - 48).to_uint64()
  }
  n
}

///|
/// The leading decimal integer of `s`, or `0`.
pub fn parse_i64(s : StringView) -> Int64 {
  let text = s.to_owned()
  let mut n : Int64 = 0L
  let mut neg = false
  let mut started = false
  for ch in text {
    let code = ch.to_int()
    if !started && code == 45 {
      neg = true
      started = true
      continue
    }
    if code < 48 || code > 57 {
      break
    }
    started = true
    n = n * 10L + (code - 48).to_int64()
  }
  if neg {
    -n
  } else {
    n
  }
}

///|
/// Percent-encode a query component, keeping only the RFC 3986 unreserved set.
pub fn url_encode(s : String) -> String {
  let mut out = ""
  for ch in s {
    let code = ch.to_int()
    let ok = (code >= 65 && code <= 90) ||
      (code >= 97 && code <= 122) ||
      (code >= 48 && code <= 57) ||
      code == 45 ||
      code == 46 ||
      code == 95 ||
      code == 126
    if ok {
      out = out + ch.to_string()
    } else {
      out = out + "%" + hex_digit(code / 16) + hex_digit(code % 16)
    }
  }
  out
}

///|
/// A single uppercase hex digit for `n` in `0..=15`.
fn hex_digit(n : Int32) -> String {
  match n {
    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"
    _ => "0"
  }
}