///|
/// The other half of SDL printing: read a type-system document back into a
/// `Schema`, so that `Schema::of_sdl(s.to_sdl())` describes the schema it was
/// printed from. The reader shares the lexer with executable documents — the
/// type system is a superset of the same grammar — and reports a bad document
/// as the same `GqlSyntaxError` (with its line and column) that `parse` does.
///
/// It reads everything `to_sdl` writes: the `schema { ... }` block and its
/// directives, directive definitions, `type` / `input` / `interface` /
/// `enum` / `union` / `scalar` definitions, descriptions, `implements`,
/// argument default values, `@deprecated(reason:)` and applied directives.
/// What it cannot recover from a printed document — a custom scalar's
/// `serialize` / `parse_value` hooks, and a directive's `on_field` hook — comes
/// back as the identity, since those are MoonBit functions and not SDL.

///|
/// A cursor over the token stream the lexer produced.
priv struct SdlCursor {
  tokens : Array[@lang.Token]
  mut pos : Int32
}

///|
/// Tokenise `text` and stand at its first token.
fn SdlCursor::of(text : String) -> SdlCursor raise @lang.GqlSyntaxError {
  { tokens: @lang.tokenize(text), pos: 0, }
}

///|
/// The token at the cursor. The lexer always ends with an `Eof`, so there is
/// always one.
fn SdlCursor::peek(self : SdlCursor) -> @lang.Token {
  self.tokens[self.pos]
}

///|
/// Consume the token at the cursor and return it. `Eof` is never consumed, so a
/// reader that runs off the end sees it forever rather than overrunning.
fn SdlCursor::bump(self : SdlCursor) -> @lang.Token {
  let t = self.tokens[self.pos]
  if !(t.kind == @lang.Eof) {
    self.pos = self.pos + 1
  }
  t
}

///|
/// Whether the token at the cursor is `kind`.
fn SdlCursor::at(self : SdlCursor, kind : @lang.TokenKind) -> Bool {
  self.peek().kind == kind
}

///|
/// Whether the reader should keep taking elements: until `kind` closes the list,
/// or until the document ends (which the following `take` turns into an error).
fn SdlCursor::until(self : SdlCursor, kind : @lang.TokenKind) -> Bool {
  !(self.at(kind) || self.at(@lang.Eof))
}

///|
/// Consume the token at the cursor, or report what was expected there.
fn SdlCursor::take(
  self : SdlCursor,
  kind : @lang.TokenKind,
  what : String,
) -> @lang.Token raise @lang.GqlSyntaxError {
  let t = self.peek()
  if !(t.kind == kind) {
    raise @lang.GqlSyntaxError("expected " + what, t.line, t.col)
  }
  self.bump()
}

///|
/// Consume a `Name` and return its text, or report what was expected there.
fn SdlCursor::name(
  self : SdlCursor,
  what : String,
) -> String raise @lang.GqlSyntaxError {
  let t = self.take(@lang.Name, what)
  t.value
}

///|
/// Consume the keyword `word` if it is next, answering whether it was there.
fn SdlCursor::word(self : SdlCursor, word : String) -> Bool {
  let t = self.peek()
  if t.kind == @lang.Name && t.value == word {
    self.bump() |> ignore
    true
  } else {
    false
  }
}

///|
/// Consume a description — a string or block-string literal immediately before
/// the element it describes — or answer `None`. An empty description is no
/// description, which is how introspection reports one too.
fn SdlCursor::description(self : SdlCursor) -> String? {
  let t = self.peek()
  if t.kind == @lang.StringVal || t.kind == @lang.BlockStringVal {
    self.bump() |> ignore
    if t.value == "" {
      None
    } else {
      Some(t.value)
    }
  } else {
    None
  }
}

///|
/// Consume a constant value — a literal, an enum name, a list or an object —
/// as the JSON an `AppliedDirective` argument or an argument default carries.
/// An enum value is kept as its name, which is what `json_to_gql_literal`
/// prints back.
fn SdlCursor::value(self : SdlCursor) -> Json raise @lang.GqlSyntaxError {
  let t = self.bump()
  match t.kind {
    @lang.IntVal | @lang.FloatVal =>
      @json.parse(t.value) catch {
        _ => Json::null()
      }
    @lang.StringVal | @lang.BlockStringVal => Json(t.value)
    @lang.Name =>
      match t.value {
        "true" => Json(true)
        "false" => Json(false)
        "null" => Json::null()
        _ => Json(t.value)
      }
    @lang.BracketL => {
      let items : Array[Json] = []
      while self.until(@lang.BracketR) {
        items.push(self.value())
      }
      self.take(@lang.BracketR, "']' to close a list value") |> ignore
      Json(items)
    }
    @lang.BraceL => {
      let out : Map[String, Json] = Map([])
      while self.until(@lang.BraceR) {
        let k = self.name("an object field name")
        self.take(@lang.Colon, "':' before an object field value") |> ignore
        out[k] = self.value()
      }
      self.take(@lang.BraceR, "'}' to close an object value") |> ignore
      Json(out)
    }
    _ => raise @lang.GqlSyntaxError("expected a value", t.line, t.col)
  }
}

///|
/// Consume a type reference — `Name`, `[T]`, and any number of `!` — as the
/// `GqlType` the schema builder speaks. A name that is one of the five built-in
/// scalars becomes a `Scalar`; every other name is a reference to a declared
/// type.
fn SdlCursor::type_ref(self : SdlCursor) -> GqlType raise @lang.GqlSyntaxError {
  let mut t = if self.at(@lang.BracketL) {
    self.bump() |> ignore
    let inner = self.type_ref()
    self.take(@lang.BracketR, "']' to close a list type") |> ignore
    ListOf(inner)
  } else {
    let n = self.name("a type name")
    if is_builtin_scalar(n) {
      Scalar(n)
    } else {
      Named(n)
    }
  }
  if self.at(@lang.Bang) {
    self.bump() |> ignore
    t = NonNull(t)
  }
  t
}

///|
/// Consume an argument list `(a: A = v, b: B)`: the declarations in order, plus
/// each defaulted argument's value keyed by name.
fn SdlCursor::arguments(
  self : SdlCursor,
) -> (Array[(String, GqlType)], Map[String, Json]) raise @lang.GqlSyntaxError {
  let args : Array[(String, GqlType)] = []
  let defaults : Map[String, Json] = Map([])
  self.take(@lang.ParenL, "'(' to open an argument list") |> ignore
  while self.until(@lang.ParenR) {
    self.description() |> ignore
    let n = self.name("an argument name")
    self.take(@lang.Colon, "':' before an argument type") |> ignore
    let t = self.type_ref()
    if self.at(@lang.Equals) {
      self.bump() |> ignore
      defaults[n] = self.value()
    }
    self.directives() |> ignore
    args.push((n, t))
  }
  self.take(@lang.ParenR, "')' to close an argument list") |> ignore
  (args, defaults)
}

///|
/// Consume a directive list `@a @b(x: 1)`, each with its constant arguments.
fn SdlCursor::directives(
  self : SdlCursor,
) -> Array[AppliedDirective] raise @lang.GqlSyntaxError {
  let out : Array[AppliedDirective] = []
  while self.at(@lang.At) {
    self.bump() |> ignore
    let name = self.name("a directive name")
    let args : Array[(String, Json)] = []
    if self.at(@lang.ParenL) {
      self.bump() |> ignore
      while self.until(@lang.ParenR) {
        let n = self.name("a directive argument name")
        self.take(@lang.Colon, "':' before a directive argument value")
        |> ignore
        args.push((n, self.value()))
      }
      self.take(@lang.ParenR, "')' to close a directive argument list")
      |> ignore
    }
    out.push(AppliedDirective::new(name, args~))
  }
  out
}

///|
/// The `reason:` of a `@deprecated`, or the reason the specification gives an
/// `@deprecated` that states none.
fn deprecated_reason_of(d : AppliedDirective) -> String {
  let mut reason = "No longer supported"
  for a in d.args {
    if a.0 == "reason" {
      match a.1 {
        String(s) => reason = s
        _ => ()
      }
    }
  }
  reason
}

///|
/// The `url:` of a `@specifiedBy`.
fn specified_url_of(d : AppliedDirective) -> String {
  let mut url = ""
  for a in d.args {
    if a.0 == "url" {
      match a.1 {
        String(s) => url = s
        _ => ()
      }
    }
  }
  url
}

///|
/// Consume one definition — the unit a type-system document is made of — and
/// record it on `schema`. `extend` is accepted and merges into the type of the
/// same name, which is how a federated subgraph's SDL reads back.
fn SdlCursor::definition(
  self : SdlCursor,
  schema : Schema,
) -> Unit raise @lang.GqlSyntaxError {
  let description = self.description()
  let extends = self.word("extend")
  let t = self.peek()
  if !(t.kind == @lang.Name) {
    raise @lang.GqlSyntaxError(
      "expected a type system definition",
      t.line,
      t.col,
    )
  }
  match t.value {
    "schema" => {
      self.bump() |> ignore
      self.schema_def(schema)
    }
    "type" => {
      self.bump() |> ignore
      self.composite(schema, Object, description, extends)
    }
    "input" => {
      self.bump() |> ignore
      self.composite(schema, Input, description, extends)
    }
    "interface" => {
      self.bump() |> ignore
      self.composite(schema, Interface, description, extends)
    }
    "enum" => {
      self.bump() |> ignore
      self.enum_def(schema, description)
    }
    "union" => {
      self.bump() |> ignore
      self.union_def(schema, description)
    }
    "scalar" => {
      self.bump() |> ignore
      self.scalar_def(schema, description)
    }
    "directive" => {
      self.bump() |> ignore
      self.directive_def(schema)
    }
    _ =>
      raise @lang.GqlSyntaxError(
        "expected a type system definition, got '\{t.value}'",
        t.line,
        t.col,
      )
  }
}

///|
/// Consume `schema @dir { query: Q mutation: M }`.
fn SdlCursor::schema_def(
  self : SdlCursor,
  schema : Schema,
) -> Unit raise @lang.GqlSyntaxError {
  for d in self.directives() {
    schema.apply_schema_directive(d)
  }
  self.take(@lang.BraceL, "'{' to open the schema definition") |> ignore
  while self.until(@lang.BraceR) {
    let root = self.name("a root operation type")
    self.take(@lang.Colon, "':' before a root operation type") |> ignore
    let name = self.name("a root operation type name")
    match root {
      "query" => schema.query = name
      "mutation" => schema.set_mutation(name)
      "subscription" => schema.set_subscription(name)
      _ =>
        raise @lang.GqlSyntaxError(
          "expected query, mutation or subscription, got '\{root}'",
          self.peek().line,
          self.peek().col,
        )
    }
  }
  self.take(@lang.BraceR, "'}' to close the schema definition") |> ignore
}

///|
/// Consume a `type` / `input` / `interface` block: its name, `implements`,
/// directives and field definitions. An `extend` merges fields into the type of
/// the same name when one is already declared.
fn SdlCursor::composite(
  self : SdlCursor,
  schema : Schema,
  kind : TypeKind,
  description : String?,
  _extends : Bool,
) -> Unit raise @lang.GqlSyntaxError {
  let name = self.name("a type name")
  let interfaces : Array[String] = []
  if self.word("implements") {
    if self.at(@lang.Amp) {
      self.bump() |> ignore
    }
    interfaces.push(self.name("an interface name"))
    while self.at(@lang.Amp) {
      self.bump() |> ignore
      interfaces.push(self.name("an interface name"))
    }
  }
  for d in self.directives() {
    schema.apply_type_directive(name, d)
  }
  self.take(@lang.BraceL, "'{' to open type " + name) |> ignore
  let fields : Array[Field] = []
  while self.until(@lang.BraceR) {
    let fdesc = self.description()
    let fname = self.name("a field name")
    let mut fargs : Array[(String, GqlType)] = []
    let mut fdefaults : Map[String, Json] = Map([])
    if self.at(@lang.ParenL) {
      let (a, d) = self.arguments()
      fargs = a
      fdefaults = d
    }
    self.take(@lang.Colon, "':' before a field type") |> ignore
    let ftyp = self.type_ref()
    let mut deprecation_reason : String? = None
    let applied : Array[AppliedDirective] = []
    for d in self.directives() {
      if d.name == "deprecated" {
        deprecation_reason = Some(deprecated_reason_of(d))
      } else {
        applied.push(d)
      }
    }
    fields.push({
      name: fname,
      args: fargs,
      typ: ftyp,
      arg_defaults: fdefaults,
      deprecation_reason,
      description: fdesc,
    })
    for d in applied {
      schema.apply_field_directive(name, fname, d)
    }
  }
  self.take(@lang.BraceR, "'}' to close type " + name) |> ignore
  let target = match schema.type_by_name(name) {
    Some(t) => t
    None => {
      let o : ObjectType = {
        name,
        fields: [],
        kind,
        interfaces: [],
        description,
      }
      schema.types.push(o)
      o
    }
  }
  for i in interfaces {
    target.interfaces.push(i)
  }
  for f in fields {
    target.fields.push(f)
  }
}

///|
/// Consume an `enum` block: its values, with each value's description and
/// `@deprecated(reason:)`.
fn SdlCursor::enum_def(
  self : SdlCursor,
  schema : Schema,
  description : String?,
) -> Unit raise @lang.GqlSyntaxError {
  let name = self.name("an enum type name")
  for d in self.directives() {
    schema.apply_type_directive(name, d)
  }
  self.take(@lang.BraceL, "'{' to open enum " + name) |> ignore
  let values : Array[String] = []
  let deprecations : Map[String, String] = Map([])
  let value_descriptions : Map[String, String] = Map([])
  while self.until(@lang.BraceR) {
    let vdesc = self.description()
    let v = self.name("an enum value")
    let mut deprecated : String? = None
    for d in self.directives() {
      if d.name == "deprecated" {
        deprecated = Some(deprecated_reason_of(d))
      }
    }
    values.push(v)
    match deprecated {
      Some(r) => deprecations[v] = r
      None => ()
    }
    match vdesc {
      Some(x) => value_descriptions[v] = x
      None => ()
    }
  }
  self.take(@lang.BraceR, "'}' to close enum " + name) |> ignore
  match schema.enum_by_name(name) {
    Some(e) =>
      for v in values {
        e.values.push(v)
      }
    None =>
      schema.enums.push({
        name,
        values,
        deprecations,
        description,
        value_descriptions,
      })
  }
}

///|
/// Consume `union Name @dir = A | B`, tolerating a leading `|`.
fn SdlCursor::union_def(
  self : SdlCursor,
  schema : Schema,
  description : String?,
) -> Unit raise @lang.GqlSyntaxError {
  let name = self.name("a union type name")
  for d in self.directives() {
    schema.apply_type_directive(name, d)
  }
  self.take(@lang.Equals, "'=' before the union member types") |> ignore
  let members : Array[String] = []
  if self.at(@lang.Pipe) {
    self.bump() |> ignore
  }
  members.push(self.name("a union member type name"))
  while self.at(@lang.Pipe) {
    self.bump() |> ignore
    members.push(self.name("a union member type name"))
  }
  match schema.union_by_name(name) {
    Some(u) =>
      for m in members {
        u.members.push(m)
      }
    None => schema.unions.push({ name, members, description, })
  }
}

///|
/// Consume `scalar Name @specifiedBy(url: "...")`.
fn SdlCursor::scalar_def(
  self : SdlCursor,
  schema : Schema,
  description : String?,
) -> Unit raise @lang.GqlSyntaxError {
  let name = self.name("a scalar type name")
  let mut spec_url : String? = None
  for d in self.directives() {
    if d.name == "specifiedBy" {
      spec_url = Some(specified_url_of(d))
    } else {
      schema.apply_type_directive(name, d)
    }
  }
  schema.scalars.push({
    name,
    serialize: fn(x) { x },
    parse_value: fn(x) { x },
    spec_url,
    description,
  })
}

///|
/// Consume `directive @name(a: A) repeatable on LOCATION | LOCATION`. An
/// executable hook cannot come from text, so a definition read back carries
/// none.
fn SdlCursor::directive_def(
  self : SdlCursor,
  schema : Schema,
) -> Unit raise @lang.GqlSyntaxError {
  self.take(@lang.At, "'@' before a directive name") |> ignore
  let name = self.name("a directive name")
  let mut args : Array[(String, GqlType)] = []
  if self.at(@lang.ParenL) {
    let (a, _) = self.arguments()
    args = a
  }
  let is_repeatable = self.word("repeatable")
  if !self.word("on") {
    let t = self.peek()
    raise @lang.GqlSyntaxError(
      "expected 'on' before the locations",
      t.line,
      t.col,
    )
  }
  let locations : Array[String] = []
  locations.push(self.name("a directive location"))
  while self.at(@lang.Pipe) {
    self.bump() |> ignore
    locations.push(self.name("a directive location"))
  }
  schema.directive_defs.push({
    name,
    locations,
    args,
    is_repeatable,
    on_field: None,
  })
}

///|
/// Build a schema from a type-system document (SDL), the inverse of `to_sdl`.
/// A document that is not a well-formed type-system definition raises
/// `GqlSyntaxError` carrying the line and column of the token that stopped it.
///
/// The hooks SDL cannot carry come back as the identity: a custom scalar's
/// `serialize` / `parse_value`, and a directive definition's `on_field`.
///
/// ```mbt nocheck
/// let s = Schema::of_sdl(
///   (
///     #|schema { query: Query }
///     #|
///     #|"The root of everything."
///     #|type Query {
///     #|  "Greets you."
///     #|  hello(name: String = "world"): String!
///     #|}
///   ),
/// )
/// // s.type_by_name("Query") carries the description and the default value.
/// ```
pub fn Schema::of_sdl(text : String) -> Schema raise @lang.GqlSyntaxError {
  let cur = SdlCursor::of(text)
  let schema = Schema::new()
  while cur.until(@lang.Eof) {
    cur.definition(schema)
  }
  schema
}