///|
/// Document-level validation rules (GraphQL spec §5), the ones that range over the
/// whole document rather than a single selection: operation-name uniqueness and
/// the lone-anonymous-operation rule (§5.2), fragment-name uniqueness (§5.5.1.1),
/// the no-unused-fragments rule (§5.5.1.4), and cycle detection on fragment
/// spreads (§5.5.2.2), plus the variable-usage collector the per-operation checks
/// (§5.8.4) rely on. These complement the per-selection rules in `validate.mbt`.

///|
/// §5.2.1.1 (Operation Name Uniqueness) and §5.2.2.1 (Lone Anonymous Operation):
/// no two operations may share a name, and an anonymous operation must be the only
/// operation in the document.
fn Validator::check_operation_names(self : Validator, doc : Document) -> Unit {
  let names : Array[String] = []
  let mut anon = 0
  let mut total = 0
  for def in doc.definitions {
    match def {
      OperationDef(op) => {
        total = total + 1
        match op.name {
          Some(n) => {
            if str_in(names, n) {
              self.err("There can be only one operation named '" + n + "'")
            }
            names.push(n)
          }
          None => anon = anon + 1
        }
      }
      FragmentDef(_) => ()
    }
  }
  if anon > 0 && total > 1 {
    self.err(
      "This anonymous operation must be the only defined operation in the document",
    )
  }
}

///|
/// §5.5.1.1 (Fragment Name Uniqueness): no two fragment definitions may share a
/// name.
fn Validator::check_fragment_uniqueness(
  self : Validator,
  doc : Document,
) -> Unit {
  let names : Array[String] = []
  for def in doc.definitions {
    match def {
      FragmentDef(fr) => {
        if str_in(names, fr.name) {
          self.err("There can be only one fragment named '" + fr.name + "'")
        }
        names.push(fr.name)
      }
      OperationDef(_) => ()
    }
  }
}

///|
/// Collect the fragment names spread anywhere within `selections`, following the
/// spreads transitively through the fragments they reference (used to compute the
/// set of fragments reachable from an operation).
fn Validator::collect_spreads(
  self : Validator,
  selections : Array[Selection],
  out : Array[String],
) -> Unit {
  for sel in selections {
    match sel {
      FieldSel(f) => self.collect_spreads(f.selection_set, out)
      FragmentSpreadSel(name, _) =>
        if not(str_in(out, name)) {
          out.push(name)
          match self.fragments.get(name) {
            Some(fr) => self.collect_spreads(fr.selection_set, out)
            None => ()
          }
        }
      InlineFragmentSel(_, _, sels) => self.collect_spreads(sels, out)
    }
  }
}

///|
/// §5.5.1.4 (Fragments Must Be Used): every defined fragment must be spread,
/// transitively, from at least one operation.
fn Validator::check_fragments_used(self : Validator, doc : Document) -> Unit {
  let reachable : Array[String] = []
  for def in doc.definitions {
    match def {
      OperationDef(op) => self.collect_spreads(op.selection_set, reachable)
      FragmentDef(_) => ()
    }
  }
  for def in doc.definitions {
    match def {
      FragmentDef(fr) =>
        if not(str_in(reachable, fr.name)) {
          self.err("Fragment '" + fr.name + "' is never used")
        }
      OperationDef(_) => ()
    }
  }
}

///|
/// The fragment names spread directly within `selections` (descending through
/// inline fragments and fields but not following a spread into its fragment body).
fn Validator::direct_spreads(
  self : Validator,
  selections : Array[Selection],
  out : Array[String],
) -> Unit {
  for sel in selections {
    match sel {
      FieldSel(f) => self.direct_spreads(f.selection_set, out)
      FragmentSpreadSel(name, _) => if not(str_in(out, name)) { out.push(name) }
      InlineFragmentSel(_, _, sels) => self.direct_spreads(sels, out)
    }
  }
}

///|
/// Whether the fragment `current` can, by following spreads, reach `start`.
/// `visited` guards against revisiting a fragment so the walk terminates even on a
/// cyclic graph.
fn Validator::fragment_reaches(
  self : Validator,
  start : String,
  current : String,
  visited : Array[String],
) -> Bool {
  match self.fragments.get(current) {
    None => false
    Some(fr) => {
      let spreads : Array[String] = []
      self.direct_spreads(fr.selection_set, spreads)
      for s in spreads {
        if s == start {
          return true
        }
        if not(str_in(visited, s)) {
          visited.push(s)
          if self.fragment_reaches(start, s, visited) {
            return true
          }
        }
      }
      false
    }
  }
}

///|
/// §5.5.2.2 (Fragment Spreads Must Not Form Cycles): a fragment must not be able
/// to spread itself, directly or transitively.
fn Validator::check_fragment_cycles(self : Validator, doc : Document) -> Unit {
  for def in doc.definitions {
    match def {
      FragmentDef(fr) =>
        if self.fragment_reaches(fr.name, fr.name, []) {
          self.err("Fragment '" + fr.name + "' spreads itself (cycle)")
        }
      OperationDef(_) => ()
    }
  }
}

///|
/// Collect the variable names referenced inside a value, descending into list and
/// object values.
fn collect_value_variables(v : Value, out : Array[String]) -> Unit {
  match v {
    Variable(n) => if not(str_in(out, n)) { out.push(n) }
    ListValue(items) =>
      for it in items {
        collect_value_variables(it, out)
      }
    ObjectValue(fields) =>
      for kv in fields {
        collect_value_variables(kv.1, out)
      }
    _ => ()
  }
}

///|
/// Collect the variable names an operation actually uses: from field and directive
/// arguments across the selection set, following fragment spreads once each. Backs
/// the §5.8.4 "all variables used" check.
fn Validator::collect_used_variables(
  self : Validator,
  selections : Array[Selection],
  visited : Map[String, Bool],
  out : Array[String],
) -> Unit {
  for sel in selections {
    match sel {
      FieldSel(f) => {
        for a in f.arguments {
          collect_value_variables(a.value, out)
        }
        for d in f.directives {
          for a in d.arguments {
            collect_value_variables(a.value, out)
          }
        }
        self.collect_used_variables(f.selection_set, visited, out)
      }
      FragmentSpreadSel(name, dirs) => {
        for d in dirs {
          for a in d.arguments {
            collect_value_variables(a.value, out)
          }
        }
        if not(visited.get(name) is Some(true)) {
          visited[name] = true
          match self.fragments.get(name) {
            Some(fr) =>
              self.collect_used_variables(fr.selection_set, visited, out)
            None => ()
          }
        }
      }
      InlineFragmentSel(_, dirs, sels) => {
        for d in dirs {
          for a in d.arguments {
            collect_value_variables(a.value, out)
          }
        }
        self.collect_used_variables(sels, visited, out)
      }
    }
  }
}