// `#gql.sdl`: the schema written first, in SDL, and everything around it generated.
//
// An object type's scalar and enum fields become a struct, and every other field becomes a
// `declare fn` for the user to fill in: root fields, fields with arguments and fields of an
// object type. The compiler then names whatever is missing. A struct the package declares
// itself takes the place of the generated one, which is where a value keeps what the schema
// does not show, such as the id behind a relation.

///|
let sdl_spec : @attr.Spec = @attr.Spec::new("gql.sdl").optional("file", Str)

///|
/// Schema text, and the expression that gives it to the generated code.
priv struct Source {
  text : String
  expr : String
  where_ : String
}

///|
/// The value of a `#|` string, which is the literal a schema is written in.
fn literal(expr : @syntax.Expr) -> String? {
  guard expr is MultilineString(elems~, ..) else { return None }
  let lines = []
  for elem in elems {
    guard elem is String(line) else { return None }
    lines.push(line)
  }
  Some(lines.join("\n"))
}

///|
/// The `#gql.sdl` among `attrs`.
fn sdl_attr(
  attrs : @list.List[@attribute.Attribute],
  file : String,
) -> @attribute.Attribute? raise {
  ignore(gql_attrs(attrs, file))
  for attr in @attr.under(attrs, "gql") {
    if @attr.name_of(attr) == Some("gql.sdl") {
      return Some(attr)
    }
  }
  None
}

///|
/// `path` beside the source file `file`.
fn beside(file : String, path : String) -> String {
  let cut = match (file.rev_find("/"), file.rev_find("\\")) {
    (Some(a), Some(b)) => Some(if a > b { a } else { b })
    (Some(a), None) | (None, Some(a)) => Some(a)
    (None, None) => None
  }
  match cut {
    Some(at) => file.exact_view(end=at + 1).to_owned() + path
    None => path
  }
}

///|
/// `text` with `\n` line endings: a checkout may turn the file's into `\r\n`, and the same schema
/// has to generate the same bytes on every system.
fn line_feeds(text : String) -> String {
  text.replace_all(old="\r\n", new="\n")
}

///|
/// The body of a `declare fn` that `#gql.sdl(file=...)` gives its text through, and the source.
fn file_source(
  attr : @attribute.Attribute,
  name : String,
  raw : @syntax.Impl,
  file : String,
  impls : Array[String],
) -> Source raise {
  let where_ = at(attr, file)
  guard sdl_spec.bind(attr).str("file") is Some(path) else {
    fail("\{where_}: `#gql.sdl` on a function names its `file`")
  }
  guard raw is TopFuncDef(decl_body=DeclNone, ..) else {
    fail("\{where_}: `#gql.sdl` goes on a `declare fn`, which it gives a body")
  }
  let text = line_feeds(
    @fs.read_file_to_string(beside(file, path)) catch {
      _ => fail("\{where_}: cannot read `\{path}`")
    },
  )
  impls.push("fn \{name}() -> String {\n  \{quoted(text)}\n}")
  { text, expr: "\{name}()", where_, }
}

///|
/// Every piece of schema text in the package.
fn sdl_sources(
  decls : Array[@gen.Decl],
  impls : Array[String],
) -> Array[Source] raise {
  let found = []
  for decl in decls {
    match decl {
      Other(file~, raw=TopLetDef(binder~, expr~, attrs~, ..)) =>
        if sdl_attr(attrs, file) is Some(attr) {
          let where_ = at(attr, file)
          if sdl_spec.bind(attr).str("file") is Some(_) {
            fail(
              "\{where_}: a `let` holds the text itself; `file` goes on a `declare fn`",
            )
          }
          guard literal(expr) is Some(text) else {
            fail("\{where_}: `#gql.sdl` reads a `#|` string")
          }
          found.push({ text, expr: binder.name, where_, })
        }
      Func(name~, attrs~, file~, raw~, ..) =>
        if sdl_attr(attrs, file) is Some(attr) {
          found.push(file_source(attr, name, raw, file, impls))
        }
      _ => ()
    }
  }
  found
}

///|
/// `gqlName` as a MoonBit identifier.
fn snake(name : String) -> String {
  let out = StringBuilder()
  for c in name {
    if c >= 'A' && c <= 'Z' {
      if !out.is_empty() {
        out.write_char('_')
      }
      out.write_char(c.to_ascii_lowercase())
    } else {
      out.write_char(c)
    }
  }
  let text = out.to_string()
  if reserved.contains(text) {
    text + "_"
  } else {
    text
  }
}

///|
/// The words a field may be called in GraphQL and an identifier may not in MoonBit.
let reserved : Array[String] = [
  "abstract", "alias", "and", "anyframe", "anytype", "as", "asm", "assert", "async",
  "atomic", "await", "break", "catch", "comptime", "const", "constructor", "continue",
  "declare", "defer", "derive", "do", "dyn", "dynclass", "dynobj", "else", "enum",
  "export", "extern", "false", "final", "finally", "fn", "for", "guard", "if", "impl",
  "import", "in", "include", "is", "lazy", "let", "local", "loop", "macro", "match",
  "member", "method", "module", "move", "mut", "namespace", "noasync", "noraise",
  "opaque", "orelse", "override", "package", "priv", "protected", "pub", "raise",
  "ref", "resume", "return", "sealed", "self", "static", "struct", "suberror", "super",
  "test", "threadlocal", "throw", "trait", "true", "try", "type", "typealias", "typeof",
  "unreachable", "unsafe", "use", "using", "var", "virtual", "void", "volatile",
  "where", "while", "with", "yield",
]

///|
/// The MoonBit type of a GraphQL type; `Json` for a custom scalar, whose shape is the user's.
fn moon_type(schema : @schema.Schema, ty : @schema.GqlType) -> String {
  let bare = (ty : @schema.GqlType) => {
    match ty {
      ListOf(inner) => "Array[\{moon_type(schema, inner)}]"
      Scalar("String" | "ID") => "String"
      Scalar("Int") => "Int32"
      Scalar("Int64") => "Int64"
      Scalar("Float") => "Double"
      Scalar("Boolean") => "Bool"
      Scalar(_) => "Json"
      Named(name) =>
        if schema.scalar_by_name(name) is Some(_) {
          "Json"
        } else {
          name
        }
      NonNull(inner) => moon_type(schema, inner)
    }
  }
  match ty {
    NonNull(inner) => bare(inner)
    other => bare(other) + "?"
  }
}

///|
/// The named type at the bottom of `ty`.
fn bottom(ty : @schema.GqlType) -> @schema.GqlType {
  match ty {
    NonNull(inner) | ListOf(inner) => bottom(inner)
    other => other
  }
}

///|
/// Whether a value of `ty` is data the parent can hold, rather than another object to resolve.
fn holds(schema : @schema.Schema, ty : @schema.GqlType) -> Bool {
  match bottom(ty) {
    Scalar(_) => true
    Named(name) =>
      schema.scalar_by_name(name) is Some(_) ||
      schema.enum_by_name(name) is Some(_)
    _ => false
  }
}

///|
/// The object types an interface or union stands for.
fn members_of(schema : @schema.Schema, name : String) -> Array[String] {
  for u in schema.unions {
    if u.name == name {
      return u.members
    }
  }
  schema.types
  .filter(t => t.kind == Object && t.interfaces.contains(name))
  .map(t => t.name)
}

///|
/// The two extensions that make the impls real methods of the type. An `impl` in the type's own
/// package promotes its methods to methods of the type, and the compiler now warns about the
/// promotion being implicit; a library package has to say it out loud.
fn extensions(name : String) -> String {
  (
    $|pub extend \{name} with @moongql.ToGql::{to_gql}
    $|pub extend \{name} with @moongql.FromGql::{from_gql}
  )
}

///|
/// The impls of a type carried as keyed fields, `pairs` being (MoonBit field, GraphQL key).
fn keyed_impls(
  name : String,
  typename : String?,
  pairs : Array[(String, String)],
) -> String {
  let writes = pairs.map(p => {
    "\{quoted(p.1)}: @moongql.ToGql::to_gql(self.\{p.0})"
  })
  if typename is Some(n) {
    writes.insert(0, "\"__typename\": \{quoted(n)}")
  }
  let reads = pairs.map(p => "\{p.0}: @moongql.read(v, \{quoted(p.1)})")
  (
    $|pub impl @moongql.ToGql for \{name} with fn to_gql(self) {
    $|  { \{writes.join(", ")} }
    $|}
    $|
    $|pub impl @moongql.FromGql for \{name} with fn from_gql(v) {
    $|  \{name}::{ \{reads.join(", ")} }
    $|}
    $|
    $|\{extensions(name)}
  )
}

///|
/// The struct and impls of an input type, whose every field is data.
fn input_type(
  schema : @schema.Schema,
  t : @schema.ObjectType,
  impls : Array[String],
) -> Unit {
  let fields = t.fields.map(f => {
    "\{snake(f.name)} : \{moon_type(schema, f.typ)}"
  })
  impls.push("pub(all) struct \{t.name} {\n  \{fields.join("\n")}\n}")
  impls.push(
    keyed_impls(t.name, None, t.fields.map(f => (snake(f.name), f.name))),
  )
}

///|
/// The `declare fn` of one field and the resolver that calls it. A root field has no value to be
/// asked on, so it takes none.
fn resolver(
  schema : @schema.Schema,
  owner : String,
  root : Bool,
  field : @schema.Field,
  out : Out,
) -> Unit {
  let params = if root { [] } else { ["self : \{owner}"] }
  let call = if root { [] } else { ["@moongql.FromGql::from_gql(info.parent)"] }
  for arg in field.args {
    let (name, ty) = arg
    let read = "info.read(\{quoted(name)})"
    if ty is NonNull(_) {
      params.push("\{snake(name)} : \{moon_type(schema, ty)}")
      call.push(read)
    } else {
      let bare = moon_type(schema, NonNull(ty))
      params.push("\{snake(name)}? : \{bare}")
      call.push("\{snake(name)}?=\{read}")
    }
  }
  let callee = snake(field.name)
  out.impls.push(
    "declare fn \{owner}::\{callee}(\{params.join(", ")}) -> \{moon_type(schema, field.typ)} raise",
  )
  let value = "\{owner}::\{callee}(\{call.join(", ")}) catch { e => raise @moongql.field_error(e) }"
  out.fields.push(
    "resolvers.field(\{quoted(owner)}, \{quoted(field.name)}, info => @moongql.ToGql::to_gql(\{value}))",
  )
}

///|
/// A subscription root field: its events are the array the declared function returns, so the
/// schema's field type is the type of one event. The field itself is in the schema already —
/// `add_sdl` put it there — so what is generated is the declaration and the two registry entries.
fn subscription_resolver(
  schema : @schema.Schema,
  owner : String,
  field : @schema.Field,
  out : Out,
) -> Unit {
  let params = []
  let call = []
  for arg in field.args {
    let (name, ty) = arg
    let read = "info.read(\{quoted(name)})"
    if ty is NonNull(_) {
      params.push("\{snake(name)} : \{moon_type(schema, ty)}")
      call.push(read)
    } else {
      let bare = moon_type(schema, NonNull(ty))
      params.push("\{snake(name)}? : \{bare}")
      call.push("\{snake(name)}?=\{read}")
    }
  }
  let callee = snake(field.name)
  let event = moon_type(schema, field.typ)
  out.impls.push(
    "declare fn \{owner}::\{callee}(\{params.join(", ")}) -> Array[\{event}] raise",
  )
  let events = "\{owner}::\{callee}(\{call.join(", ")}) catch { e => raise @moongql.field_error(e) }"
  out.subscriptions.push(
    "subscribers.field(gql_subscription.name, \{quoted(field.name)}, info => (\{events}).map(e => @moongql.ToGql::to_gql(e)))",
  )
  out.subscriptions.push(
    "resolvers.field(gql_subscription.name, \{quoted(field.name)}, info => info.parent)",
  )
}

///|
/// The struct of an object type, the package's own when it declares one, and a resolver for each
/// field the struct does not answer.
fn object_type(
  schema : @schema.Schema,
  t : @schema.ObjectType,
  bound : Map[String, Array[String]],
  out : Out,
) -> Unit {
  let plain = (f : @schema.Field) => {
    f.args.is_empty() &&
    (match bound.get(t.name) {
      Some(names) => names.contains(snake(f.name))
      None => holds(schema, f.typ)
    })
  }
  let kept = t.fields.filter(plain)
  let pairs = match bound.get(t.name) {
    Some(names) =>
      names.map(name => {
        let sdl = kept.filter(f => snake(f.name) == name)
        (name, if sdl is [f] { f.name } else { camel(name) })
      })
    None => kept.map(f => (snake(f.name), f.name))
  }
  if !bound.contains(t.name) {
    let fields = kept.map(f => "\{snake(f.name)} : \{moon_type(schema, f.typ)}")
    out.impls.push("pub(all) struct \{t.name} {\n  \{fields.join("\n")}\n}")
  }
  out.impls.push(keyed_impls(t.name, Some(t.name), pairs))
  for f in t.fields {
    if !plain(f) {
      resolver(schema, t.name, false, f, out)
    }
  }
}

///|
/// An interface or union as an enum over the object types it stands for.
fn abstract_of(
  schema : @schema.Schema,
  name : String,
  impls : Array[String],
) -> Unit {
  let members = members_of(schema, name)
  let cases = members.map(m => "\{m}(\{m})")
  let writes = members.map(m => "\{m}(v) => @moongql.ToGql::to_gql(v)")
  let reads = members.map(m => {
    "{ \"__typename\": String(\{quoted(m)}), .. } => \{m}(@moongql.FromGql::from_gql(v))"
  })
  impls.push(
    (
      $|pub(all) enum \{name} {
      $|  \{cases.join("\n")}
      $|}
      $|
      $|pub impl @moongql.ToGql for \{name} with fn to_gql(self) {
      $|  match self {
      $|    \{writes.join("\n")}
      $|  }
      $|}
      $|
      $|pub impl @moongql.FromGql for \{name} with fn from_gql(v) {
      $|  match v {
      $|    \{reads.join("\n")}
      $|    other => raise @moongql.ResolverError::expected(\{quoted(name)}, other)
      $|  }
      $|}
      $|
      $|\{extensions(name)}
    ),
  )
}

///|
/// An enum, its constructors named after its values.
fn enum_of(e : @schema.EnumType, impls : Array[String]) -> Unit {
  let case_of = (value : String) => {
    match value.get_char(0) {
      Some(c) if c >= 'a' && c <= 'z' =>
        c.to_ascii_uppercase().to_string() +
        value.exact_view(start=1).to_owned()
      _ => value
    }
  }
  let writes = e.values.map(v => "\{case_of(v)} => \{quoted(v)}")
  let reads = e.values.map(v => "String(\{quoted(v)}) => \{case_of(v)}")
  impls.push(
    (
      $|#derive(Eq, Debug)
      $|pub(all) enum \{e.name} {
      $|  \{e.values.map(case_of).join("\n")}
      $|}
      $|
      $|pub impl @moongql.ToGql for \{e.name} with fn to_gql(self) {
      $|  match self {
      $|    \{writes.join("\n")}
      $|  }
      $|}
      $|
      $|pub impl @moongql.FromGql for \{e.name} with fn from_gql(v) {
      $|  match v {
      $|    \{reads.join("\n")}
      $|    other => raise @moongql.ResolverError::expected(\{quoted(e.name)}, other)
      $|  }
      $|}
      $|
      $|\{extensions(e.name)}
    ),
  )
}

///|
/// The plain structs of the package, by name, with their field names; a struct `#gql.type` has
/// claimed is code first and clashes with a type of the same name in the schema.
fn bindable(
  decls : Array[@gen.Decl],
  schema : @schema.Schema,
  where_ : String,
) -> Map[String, Array[String]] raise {
  let found : Map[String, Array[String]] = Map([])
  for decl in decls {
    if decl is Struct(name~, fields~, attrs~, file~, ..) &&
      schema.type_by_name(name) is Some(_) {
      if gql_attrs(attrs, file) is [_, ..] {
        fail(
          "\{where_}: `\{name}` is in the schema and also annotated; bind the struct by leaving its `#gql.*` off",
        )
      }
      found[name] = fields.map(f => f.name)
    }
  }
  found
}

///|
/// Everything `#gql.sdl` in the package generates, into `out`.
fn schema_first(decls : Array[@gen.Decl], out : Out) -> Unit raise {
  let sources = sdl_sources(decls, out.impls)
  guard sources is [first, ..] else { return }
  let text = sources.map(s => s.text).join("\n")
  let schema = @schema.Schema::of_sdl(text) catch {
    e => fail("\{first.where_}: \{e}")
  }
  let bound = bindable(decls, schema, first.where_)
  out.types.push(
    "schema.add_sdl(\{sources.map(s => s.expr).join(" + \"\\n\" + ")}) catch { e => abort(e.to_string()) }",
  )
  let roots = [schema.query]
  if schema.mutation is Some(name) {
    roots.push(name)
  }
  let subscription = schema.subscription
  for t in schema.types {
    match t.kind {
      Input => input_type(schema, t, out.impls)
      Interface => abstract_of(schema, t.name, out.impls)
      Object =>
        if roots.contains(t.name) {
          out.impls.push("pub(all) struct \{t.name} {}")
          for f in t.fields {
            resolver(schema, t.name, true, f, out)
          }
        } else if Some(t.name) == subscription {
          out.impls.push("pub(all) struct \{t.name} {}")
          for f in t.fields {
            subscription_resolver(schema, t.name, f, out)
          }
        } else {
          object_type(schema, t, bound, out)
        }
    }
  }
  for e in schema.enums {
    enum_of(e, out.impls)
  }
  for u in schema.unions {
    abstract_of(schema, u.name, out.impls)
  }
}