///|
priv struct Cursor {
  text : String
  mut offset : Int32
}

///|
// Run before the standard parser so malformed, deeply nested input cannot
// consume its default 1024-frame nesting allowance.
fn guard_depth(text : String, path : String) -> Unit raise InputError {
  let mut quoted = false
  let mut escaped = false
  let mut depth = 0
  for ch in text {
    if quoted {
      if escaped {
        escaped = false
      } else if ch == '\\' {
        escaped = true
      } else if ch == '"' {
        quoted = false
      }
    } else if ch == '"' {
      quoted = true
    } else if ch == '{' || ch == '[' {
      depth += 1
      if depth > 32 {
        invalid(path, "nesting exceeds 32")
      }
    } else if ch == '}' || ch == ']' {
      depth -= 1
    }
  }
}

///|
fn Cursor::space(self : Cursor) -> Unit {
  while self.offset < self.text.length() {
    let c = self.text[self.offset]
    if c == 32 || c == 9 || c == 10 || c == 13 {
      self.offset += 1
    } else {
      break
    }
  }
}

///|
// All cursor operations run only after the standard parser has accepted
// the complete document, so token and delimiter positions are valid.
fn Cursor::quoted(self : Cursor) -> String {
  let start = self.offset
  self.offset += 1
  while self.offset < self.text.length() {
    let c = self.text[self.offset]
    self.offset += 1
    if c == 92 {
      self.offset += 1
    } else if c == 34 {
      break
    }
  }
  self.text[start:self.offset].to_owned()
}

///|
fn Cursor::value(self : Cursor, path : String) -> Unit raise InputError {
  self.space()
  let c = self.text[self.offset]
  if c == 123 {
    self.offset += 1
    self.space()
    let keys : Map[String, Bool] = Map([])
    while self.text[self.offset] != 125 {
      let encoded = self.quoted()
      let decoded = @json.parse(encoded) catch {
        _ => invalid(path, "invalid object key")
      }
      let key = text_at(decoded, path)
      if keys.contains(key) {
        invalid(path, "duplicate object key")
      }
      keys[key] = true
      self.space()
      self.offset += 1 // colon
      self.value(path)
      self.space()
      if self.text[self.offset] == 125 {
        break
      }
      self.offset += 1 // comma
      self.space()
    }
    self.offset += 1
  } else if c == 91 {
    self.offset += 1
    self.space()
    while self.text[self.offset] != 93 {
      self.value(path)
      self.space()
      if self.text[self.offset] == 93 {
        break
      }
      self.offset += 1
      self.space()
    }
    self.offset += 1
  } else if c == 34 {
    self.quoted() |> ignore
  } else {
    let start = self.offset
    while self.offset < self.text.length() {
      let c = self.text[self.offset]
      if c == 44 ||
        c == 93 ||
        c == 125 ||
        c == 32 ||
        c == 9 ||
        c == 10 ||
        c == 13 {
        break
      }
      self.offset += 1
    }
    // All accepted schemas use integer numeric fields. Inspect the original
    // token here: the standard JSON parser may discard the decimal spelling.
    if c == 45 || (c >= 48 && c <= 57) {
      exact_integer(self.text[start:self.offset].to_owned(), path) |> ignore
    }
  }
}