// The `fields:` argument of `@requires` and `@provides`: a selection set written
// as a string, read back so that a subgraph can be told its paths do not exist.

///|
/// What a field set that is not one raises inside this reader; `read_field_set`
/// turns it into `None`, since a bad set is reported by whoever asked.
priv suberror BadFieldSet

///|
/// One name in a federation field set, with the sub-selection written under it:
/// `"weight"` is one entry with no children, `"author { name }"` is `author` with
/// `name` inside.
priv struct FieldSetEntry {
  name : String
  children : Array[FieldSetEntry]
}

///|
/// Read a field set — names, each with an optional `{ ... }` sub-selection — or
/// `None` when the text is not one (an unclosed brace, a punctuation mark where a
/// name belongs, an empty set).
fn read_field_set(text : StringView) -> Array[FieldSetEntry]? {
  let chars = []
  for c in text {
    chars.push(c)
  }
  let (entries, at) = read_field_set_entries(chars, 0) catch {
    BadFieldSet => return None
  }
  if entries.is_empty() || at != chars.length() {
    return None
  }
  Some(entries)
}

///|
/// The entries at the current depth, and the index just past them. A `}` ends the
/// list; whitespace and commas separate names, and a `{` after a name opens its
/// sub-selection.
fn read_field_set_entries(
  chars : Array[Char],
  start : Int32,
) -> (Array[FieldSetEntry], Int32) raise BadFieldSet {
  let entries : Array[FieldSetEntry] = []
  let mut at = start
  while at < chars.length() {
    let c = chars[at]
    if c == '}' || c == '{' {
      break
    }
    if is_set_space(c) {
      at = at + 1
      continue
    }
    if !is_set_name_char(c) {
      raise BadFieldSet
    }
    let begin = at
    while at < chars.length() && is_set_name_char(chars[at]) {
      at = at + 1
    }
    let name = chars_to_string(chars, begin, at)
    // A `{` after the name opens its sub-selection; anything else ends the entry.
    let mut probe = at
    while probe < chars.length() && is_set_space(chars[probe]) {
      probe = probe + 1
    }
    let children = if probe < chars.length() && chars[probe] == '{' {
      let (inner, next) = read_field_set_entries(chars, probe + 1)
      // The `}` that closes this sub-selection is the one the inner list stopped at.
      if next >= chars.length() || chars[next] != '}' {
        raise BadFieldSet
      }
      at = next + 1
      inner
    } else {
      []
    }
    entries.push({ name, children, })
  }
  (entries, at)
}

///|
/// Whether `c` separates field-set names.
fn is_set_space(c : Char) -> Bool {
  c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == ','
}

///|
/// Whether `c` may appear in a field-set name.
fn is_set_name_char(c : Char) -> Bool {
  c == '_' ||
  (c >= 'a' && c <= 'z') ||
  (c >= 'A' && c <= 'Z') ||
  (c >= '0' && c <= '9')
}

///|
/// The characters `[start, end)` of `chars` as a string.
fn chars_to_string(chars : Array[Char], start : Int32, end : Int32) -> String {
  let out = StringBuilder()
  for i in start..