///|
/// Preserve the Number subtype returned by the pinned reference implementation.
pub(all) enum ConfigNumber {
  Integer(Int)
  Long(Int64)
  Floating(Double)
} derive(Debug, Eq)

///|
pub impl ToJson for ConfigNumber with fn to_json(self) {
  match self {
    Integer(n) => Json::object({ "kind": "int", "value": n.to_string() })
    Long(n) => Json::object({ "kind": "long", "value": n.to_string() })
    Floating(n) => {
      let value = if n.is_nan() {
        "NaN".to_json()
      } else if n.is_inf() {
        (if n > 0.0 { "Infinity" } else { "-Infinity" }).to_json()
      } else if n == 0.0 && n.reinterpret_as_int64() < 0L {
        "-0.0".to_json()
      } else {
        n.to_json()
      }
      let bits = if n.is_nan() {
        9221120237041090560L
      } else {
        n.reinterpret_as_int64()
      }
      Json::object({
        "kind": "double",
        "value": value,
        "bits": bits.to_string(),
      })
    }
  }
}

///|
fn java_integer_value(text : String) -> Int64? {
  if text.is_empty() || text.length() > 10000 {
    return None
  }
  let start = if text[0] == 43 || text[0] == 45 { 1 } else { 0 }
  if start == text.length() {
    return None
  }
  let mut ascii = true
  for i = start; i < text.length(); i = i + 1 {
    let c = text[i]
    if c >= 128 {
      ascii = false
      break
    }
    if c < 48 || c > 57 {
      return None
    }
  }
  if ascii {
    return Some(@string.parse_int64(text)) catch { _ => None }
  }
  let out = StringBuilder()
  let mut first = true
  let mut digits = 0
  for c in text.iter() {
    if first && (c == '+' || c == '-') {
      out.write_char(c)
    } else {
      let digit = numeric_digit(c.to_int())
      if digit < 0 {
        return None
      }
      out.write_char((48 + digit).unsafe_to_char())
      digits += 1
    }
    first = false
  }
  if digits == 0 {
    return None
  }
  Some(@string.parse_int64(out.to_string())) catch {
    _ => None
  }
}

///|
fn numeric_index(text : String) -> Int {
  if !text.is_empty() && text[0] < 128 {
    let fast = Some(@string.parse_int(text)) catch { _ => None }
    if fast is Some(index) {
      return if index >= 0 { index } else { -1 }
    }
  }
  match java_integer_value(text) {
    Some(n) => if n >= 0L && n <= 2147483647L { n.to_int() } else { -1 }
    None => -1
  }
}

///|
fn compact_number(n : Int64) -> ConfigNumber {
  if n >= -2147483648L && n <= 2147483647L {
    Integer(n.to_int())
  } else {
    Long(n)
  }
}

///|
pub fn get_number(
  config : Value,
  key : String,
) -> ConfigNumber raise ParseError {
  number_value(required(config, key))
}

///|
fn number_value(value : Value) -> ConfigNumber raise ParseError {
  match value {
    Number(text) =>
      match java_integer_value(text) {
        Some(n) => compact_number(n)
        None => {
          let n = double_value(text)
          let integer = java_long(n)
          if !integer_spelling(text) && integer.to_double() == n {
            compact_number(integer)
          } else {
            Floating(n)
          }
        }
      }
    Text(text) =>
      match java_integer_value(text) {
        Some(n) => Long(n)
        None => Floating(double_value(text))
      }
    _ => raise Invalid("expected number-compatible value")
  }
}

///|
pub fn get_number_list(
  config : Value,
  key : String,
) -> Array[ConfigNumber] raise ParseError {
  typed_list(config, key, value => number_value(required_list_value(value)))
}

///|
pub fn get_object(
  config : Value,
  key : String,
) -> Map[String, Value] raise ParseError {
  object_value(required(config, key))
}

///|
fn object_value(value : Value) -> Map[String, Value] raise ParseError {
  match copy_value(value) {
    Object(fields) | SealedObject(fields) => fields
    _ => raise Invalid("expected object-compatible value")
  }
}

///|
pub fn get_object_list(
  config : Value,
  key : String,
) -> Array[Map[String, Value]] raise ParseError {
  typed_list(config, key, value => object_value(required_list_value(value)))
}

///|
fn unwrapped_tree(
  value : Value,
  depth : Int,
  work : Work,
) -> Value raise ParseError {
  tree_step(value, depth, work)
  match value {
    Bare(text) => Text(text)
    Text(_) | Number(_) | Boolean(_) | Null => value
    Object(fields) | SealedObject(fields) => {
      let out = Map([])
      for key, child in fields {
        work.spend(key.length())
        out[key] = unwrapped_tree(child, depth + 1, work)
      }
      Object(out)
    }
    List(items) =>
      List(items.map(value => unwrapped_tree(value, depth + 1, work)))
    _ => raise Invalid("cannot unwrap unresolved value")
  }
}

///|
pub fn get_any_ref(config : Value, key : String) -> Value raise ParseError {
  unwrapped_tree(required(config, key), 0, tree_work())
}

///|
pub fn get_any_ref_list(
  config : Value,
  key : String,
) -> Array[Value] raise ParseError {
  match unwrapped_tree(List(get_as_list(config, key)), 0, tree_work()) {
    List(items) => items
    _ => abort("list copy invariant")
  }
}

///|
/// Match exact case-sensitive enum names; values may be a caller-defined enum.
pub fn[T] get_enum(
  config : Value,
  key : String,
  choices : Map[String, T],
) -> T raise ParseError {
  let name = get_as_string(config, key)
  choices
  .get(name)
  .unwrap_or_else(() => raise Invalid("unknown enum constant: " + name))
}

///|
pub fn[T] get_enum_list(
  config : Value,
  key : String,
  choices : Map[String, T],
) -> Array[T] raise ParseError {
  get_as_list(config, key).map(value => {
    let name = string_value(value)
    choices
    .get(name)
    .unwrap_or_else(() => raise Invalid("unknown enum constant: " + name))
  })
}