///|
fn required(config : Value, key : String) -> Value raise ParseError {
  match required_document_value(config, key) {
    Bare(s) => Text(s)
    value => value
  }
}

///|
pub fn has_path(
  config : Value,
  key : String,
  include_null? : Bool = false,
) -> Bool {
  match get(config, key) {
    None => false
    Some(Null) => include_null
    _ => true
  }
}

///|
pub fn get_is_null(config : Value, key : String) -> Bool raise ParseError {
  match peek_document_path(config, key) {
    None => raise Invalid("missing key: " + key)
    Some(Null) => true
    Some(value) =>
      if uncertain(value) && !known_object(value) {
        raise Invalid("unresolved path: " + key)
      } else {
        false
      }
  }
}

///|
/// HOCON's recommended conversions, separate from the original strict getters.
pub fn get_as_string(config : Value, key : String) -> String raise ParseError {
  string_value(required(config, key))
}

///|
fn string_value(value : Value) -> String raise ParseError {
  match value {
    Text(s) | Bare(s) | Number(s) => s
    Boolean(b) => if b { "true" } else { "false" }
    _ => raise Invalid("expected string-compatible scalar")
  }
}

///|
pub fn get_as_bool(config : Value, key : String) -> Bool raise ParseError {
  bool_value(required(config, key))
}

///|
fn bool_value(value : Value) -> Bool raise ParseError {
  match value {
    Boolean(b) => b
    Text("true" | "yes" | "on") => true
    Text("false" | "no" | "off") => false
    _ => raise Invalid("expected boolean-compatible value")
  }
}

///|
fn number_text(v : Value) -> String raise ParseError {
  match v {
    Text(s) | Number(s) => s
    _ => raise Invalid("expected number-compatible value")
  }
}

///|
fn double_value(text : String) -> Double raise ParseError {
  if text.length() > 10000 {
    raise Invalid("number length limit")
  }
  let mut start = 0
  let mut end = text.length()
  while start < end && text[start] <= 32 {
    start += 1
  }
  while end > start && text[end - 1] <= 32 {
    end -= 1
  }
  let negative = start < end && text[start] == 45
  if start < end && (text[start] == 43 || text[start] == 45) {
    start += 1
  }
  let unsigned = text.view(start_offset=start, end_offset=end)
  if unsigned == "NaN" {
    return @string.parse_double(if negative { "-NaN" } else { "NaN" }) catch {
      _ => raise Invalid("invalid number")
    }
  }
  if unsigned == "Infinity" {
    return if negative { -1.0 / 0.0 } else { 1.0 / 0.0 }
  }
  if end > start &&
    (
      text[end - 1] == 102 ||
      text[end - 1] == 70 ||
      text[end - 1] == 100 ||
      text[end - 1] == 68
    ) {
    end -= 1
  }
  let unsigned = text.view(start_offset=start, end_offset=end).to_owned()
  let value = if unsigned.has_prefix("0x") || unsigned.has_prefix("0X") {
    hex_double(unsigned)
  } else {
    decimal_double(unsigned) catch {
      _ => raise Invalid("invalid number: " + text)
    }
  }
  if negative {
    -value
  } else {
    value
  }
}

///|
pub fn get_double(config : Value, key : String) -> Double raise ParseError {
  number_double(required(config, key))
}

///|
fn number_double(value : Value) -> Double raise ParseError {
  let text = number_text(value)
  if value is Text(_) {
    for i = 0; i < text.length(); i = i + 1 {
      if text[i] >= 128 {
        // String integers from BMP digit blocks should not pay for an
        // expected floating-grammar exception on every successful read.
        return match java_integer_value(text) {
          Some(integer) => integer.to_double()
          None => double_value(text)
        }
      }
    }
  }
  let number = double_value(text) catch {
    error =>
      // Java's integer conversion also accepts BMP decimal digit blocks.
      // Those spellings are outside the decimal/hex floating grammar.
      match java_integer_value(text) {
        Some(integer) => integer.to_double()
        None => raise error
      }
  }
  // Subtype compaction followed by doubleValue() leaves all nonzero values
  // unchanged. A source Number's zero is compacted; a String's integer zero
  // is first parsed as Long, while floating/trimmed spellings retain -0.0.
  if number == 0.0 && (value is Number(_) || integer_spelling(text)) {
    0.0
  } else {
    number
  }
}

///|
fn java_long(value : Double) -> Int64 {
  if value.is_nan() {
    0L
  } else if value >= 9223372036854775807.0 {
    9223372036854775807L
  } else if value <= -9223372036854775808.0 {
    -9223372036854775808L
  } else {
    value.to_int64()
  }
}

///|
fn long_value(text : String) -> Int64 raise ParseError {
  java_integer_value(text).unwrap_or_else(() => java_long(double_value(text)))
}

///|
pub fn get_long(config : Value, key : String) -> Int64 raise ParseError {
  long_value(number_text(required(config, key)))
}

///|
pub fn get_as_int(config : Value, key : String) -> Int raise ParseError {
  let n = get_long(config, key)
  if n < -2147483648L || n > 2147483647L {
    raise Invalid("integer out of 32-bit range")
  }
  n.to_int()
}

///|
pub fn get_as_list(
  config : Value,
  key : String,
) -> Array[Value] raise ParseError {
  match required(config, key) {
    List(xs) => xs.copy()
    value => collection_values(value)
  }
}

///|
// Internal readers only borrow the input array while constructing a fresh
// converted result. The public get_as_list API still returns a copied array.
fn collection_values(value : Value) -> Array[Value] raise ParseError {
  match value {
    List(xs) => xs
    Object(m) | SealedObject(m) => {
      let indexed : Map[Int, Value] = Map([])
      for name in object_iteration_keys(m) {
        let n = numeric_index(name)
        if n >= 0 {
          indexed[n] = m[name]
        }
      }
      let indices = indexed.keys().collect()
      if indices.is_empty() {
        raise Invalid("object has no numeric list indices")
      }
      indices.sort()
      indices.map(index => indexed[index])
    }
    _ => raise Invalid("expected list-compatible value")
  }
}

///|
fn trim_space(text : String) -> String {
  let cs = text.to_array()
  let mut start = 0
  let mut end = cs.length()
  while start < end && whitespace(cs[start]) {
    start += 1
  }
  while end > start && whitespace(cs[end - 1]) {
    end -= 1
  }
  String::from_array(cs[start:end])
}

///|
fn quantity(text : String) -> (String, String) raise ParseError {
  if text.length() > 10000 {
    raise Invalid("quantity length limit")
  }
  let text = trim_space(text)
  let cs = text.to_array()
  let mut end = cs.length()
  while end > 0 &&
        (
          (cs[end - 1] >= 'a' && cs[end - 1] <= 'z') ||
          (cs[end - 1] >= 'A' && cs[end - 1] <= 'Z') ||
          cs[end - 1].to_int() > 127
        ) {
    end -= 1
  }
  let number = trim_space(String::from_array(cs[:end]))
  let unit = String::from_array(cs[end:])
  if number.is_empty() {
    raise Invalid("quantity requires a number")
  }
  (number, unit)
}

///|
fn integer_spelling(s : String) -> Bool {
  let start = if !s.is_empty() && (s[0] == 45 || s[0] == 43) { 1 } else { 0 }
  if s.length() <= start {
    return false
  }
  for i = start; i < s.length(); i = i + 1 {
    if s[i] < 48 || s[i] > 57 {
      return false
    }
  }
  true
}

///|
fn scale_time(value : Int64, factor : Int64) -> Int64 {
  // All callers supply a positive unit ratio. Check before multiplying to
  // reproduce TimeUnit saturation without arbitrary-precision allocation.
  if value > 9223372036854775807L / factor {
    9223372036854775807L
  } else if value < -9223372036854775808L / factor {
    -9223372036854775808L
  } else {
    value * factor
  }
}

///|
/// Duration in nanoseconds; unitless values use milliseconds.
pub fn get_duration(config : Value, key : String) -> Int64 raise ParseError {
  duration_text(number_text(required(config, key)))
}

///|
fn duration_text(text : String) -> Int64 raise ParseError {
  let (n, unit) = quantity(text)
  let multiplier : Int64 = match unit {
    "ns" | "nano" | "nanos" | "nanosecond" | "nanoseconds" => 1L
    "us" | "micro" | "micros" | "microsecond" | "microseconds" => 1000L
    "" | "ms" | "milli" | "millis" | "millisecond" | "milliseconds" => 1000000L
    "s" | "second" | "seconds" => 1000000000L
    "m" | "minute" | "minutes" => 60000000000L
    "h" | "hour" | "hours" => 3600000000000L
    "d" | "day" | "days" => 86400000000000L
    _ => raise Invalid("unknown duration unit: " + unit)
  }
  if integer_spelling(n) {
    let whole = @string.parse_int64(n) catch {
      _ => raise Invalid("duration integer overflow")
    }
    scale_time(whole, multiplier)
  } else {
    java_long(double_value(n) * multiplier.to_double())
  }
}

///|
fn byte_multiplier(unit : String) -> @bigint.BigInt raise ParseError {
  if ["", "b", "B", "byte", "bytes"].contains(unit) {
    return @bigint.BigInt::from_int(1)
  }
  let names = [
    ("k", "kilo", "kibi"),
    ("M", "mega", "mebi"),
    ("G", "giga", "gibi"),
    ("T", "tera", "tebi"),
    ("P", "peta", "pebi"),
    ("E", "exa", "exbi"),
    ("Z", "zetta", "zebi"),
    ("Y", "yotta", "yobi"),
  ]
  for i, (letter, decimal, binary) in names {
    let decimal_units = [letter + "B", decimal + "byte", decimal + "bytes"]
    let upper = letter.to_upper()
    let binary_units = [
      upper,
      upper.to_lower(),
      upper + "i",
      upper + "iB",
      binary + "byte",
      binary + "bytes",
    ]
    let base = if decimal_units.contains(unit) {
      1000
    } else if binary_units.contains(unit) {
      1024
    } else {
      continue
    }
    return @bigint.BigInt::from_int(base).pow(@bigint.BigInt::from_int(i + 1))
  }
  raise Invalid("unknown byte-size unit: " + unit)
}

///|
fn decimal_scaled(
  text : String,
  multiplier : @bigint.BigInt,
) -> @bigint.BigInt raise ParseError {
  let source = trim_space(text)
  let negative = source.has_prefix("-")
  let source = if source.has_prefix("+") || negative {
    source[1:].to_owned()
  } else {
    source
  }
  let cs = source.to_array()
  let digits = StringBuilder()
  let mut i = 0
  let mut fraction = 0
  let mut dotted = false
  let mut count = 0
  while i < cs.length() {
    let c = cs[i]
    if c >= '0' && c <= '9' {
      digits.write_char(c)
      count += 1
      if dotted {
        fraction += 1
      }
    } else if c == '.' && !dotted {
      dotted = true
    } else {
      break
    }
    i += 1
  }
  if count == 0 {
    raise Invalid("invalid byte-size number")
  }
  let exponent = if i < cs.length() && (cs[i] == 'e' || cs[i] == 'E') {
    i += 1
    let exp = String::from_array(cs[i:])
    i = cs.length()
    @string.parse_int(exp) catch {
      _ => raise Invalid("invalid byte-size exponent")
    }
  } else {
    0
  }
  if i != cs.length() || exponent > 4096 || exponent < -4096 {
    raise Invalid("invalid or excessive byte-size exponent")
  }
  let mut value = @bigint.BigInt::from_string(digits.to_string()) * multiplier
  let shift = exponent - fraction
  if shift > 0 {
    value = value *
      @bigint.BigInt::from_int(10).pow(@bigint.BigInt::from_int(shift))
  } else if shift < 0 {
    value = value /
      @bigint.BigInt::from_int(10).pow(@bigint.BigInt::from_int(-shift))
  }
  if negative {
    -value
  } else {
    value
  }
}

///|
fn memory_size(v : Value) -> @bigint.BigInt raise ParseError {
  let value = match v {
    Number(text) => @bigint.BigInt::from_int64(long_value(text))
    Text(text) => {
      let number = Some(long_value(text)) catch { _ => None }
      match number {
        Some(n) => @bigint.BigInt::from_int64(n)
        None => {
          let (n, unit) = quantity(text)
          decimal_scaled(n, byte_multiplier(unit))
        }
      }
    }
    _ => raise Invalid("expected size-compatible value")
  }
  if value.compare_int(0) < 0 {
    raise Invalid("memory size cannot be negative")
  }
  value
}

///|
/// Exact nonnegative byte count, including units larger than int64.
pub fn get_memory_size(config : Value, key : String) -> String raise ParseError {
  memory_size(required(config, key)).to_string()
}

///|
pub fn get_bytes(config : Value, key : String) -> Int64 raise ParseError {
  let value = memory_size(required(config, key))
  if value.compare_int64(9223372036854775807L) > 0 {
    raise Invalid("byte count out of int64 range")
  }
  value.to_int64()
}