///|
fn whitespace(char : Char) -> Bool {
  char == ' ' || char == '\n' || char == '\r' || char == '\t'
}

///|
fn nesting_budget(chars : Array[Char]) -> Unit raise @ledger.InputError {
  let mut depth = 0
  let mut quoted = false
  let mut escaped = false
  for char in chars {
    if quoted {
      if escaped {
        escaped = false
      } else if char == '\\' {
        escaped = true
      } else if char == '"' {
        quoted = false
      }
    } else if char == '"' {
      quoted = true
    } else if char is ('{' | '[') {
      depth += 1
      if depth > 64 {
        raise bad(
          "JsonDepthLimit", "$", "JSON nesting exceeds the supported depth of 64",
        )
      }
    } else if char is ('}' | ']') {
      depth = (depth - 1).max(0)
    }
  }
}

///|
// The standard parser validates syntax and depth first. This narrow second
// pass rejects duplicate *decoded* object keys that its Map representation
// would otherwise overwrite. It is not a second JSON implementation.
fn strict_json(
  source : String,
  limits : @ledger.Limits,
) -> Json raise @ledger.InputError {
  limits.validate()
  if source.length() > limits.max_source_units {
    raise bad(
      "SourceLimit", "$", "JSON source exceeds the configured UTF-16 length limit",
    )
  }
  let chars = source.to_array()
  nesting_budget(chars)
  let value = @json.parse(source) catch {
    error => raise bad("InvalidJson", "$", error.to_string())
  }
  let objects : Array[Map[String, Unit]] = []
  let mut i = 0
  let mut line = 1
  let mut column = 1
  while i < chars.length() {
    let char = chars[i]
    if char == '{' {
      objects.push(Map([]))
    } else if char == '}' {
      ignore(objects.pop())
    } else if char == '"' {
      let start = i
      let start_line = line
      let start_column = column
      i += 1
      column += 1
      while i < chars.length() && chars[i] != '"' {
        if chars[i] == '\\' {
          i += 1
          column += 1
        }
        i += 1
        column += 1
      }
      let mut next = i + 1
      while next < chars.length() && whitespace(chars[next]) {
        next += 1
      }
      if next < chars.length() && chars[next] == ':' {
        let encoded = String::from_array(chars[start:i + 1])
        let decoded = @json.parse(encoded) catch {
          error => raise bad("InvalidJson", "$", error.to_string())
        }
        let key = match decoded {
          String(key) => key
          _ => raise bad("InvalidJson", "$", "Object key is not a string")
        }
        guard objects.last() is Some(seen) else {
          raise bad("InvalidJson", "$", "Object key appears outside an object")
        }
        if seen.contains(key) {
          raise @ledger.InputError::Invalid({
            code: "DuplicateJsonKey",
            report_id: "",
            path: "$",
            message: "Duplicate decoded object key: " + key,
            line: start_line,
            column: start_column,
          })
        }
        seen[key] = ()
        // The core parser may discard decimal lexemes after Double rounding.
        // Check our integer fields against authored digits before using that
        // projection. Type errors are handled by the schema decoder below.
        if key == "schema" || key == "attempt" || key == "utf8_bytes" {
          let mut token_start = next + 1
          while token_start < chars.length() && whitespace(chars[token_start]) {
            token_start += 1
          }
          if token_start < chars.length() &&
            (
              chars[token_start] == '-' ||
              (chars[token_start] >= '0' && chars[token_start] <= '9')
            ) {
            let mut token_end = token_start + 1
            while token_end < chars.length() &&
                  chars[token_end] is ('0'..<='9' | '-' | '+' | '.' | 'e' | 'E') {
              token_end += 1
            }
            let token = String::from_array(chars[token_start:token_end])
            if exact_positive_integer(token) is None {
              raise bad(
                "ExpectedInteger",
                "$/" + key,
                "Expected an exact positive 32-bit integer; fractional digits cannot be rounded",
              )
            }
          }
        }
      }
    }
    if char == '\n' {
      line += 1
      column = 1
    } else {
      column += 1
    }
    i += 1
  }
  value
}