///|
/// What an annotated type became in the schema.
#derive(Eq)
priv enum Shape {
  Object
  Input
  Enum
  Union
  Interface
}

///|
/// An annotated type: its GraphQL name and shape.
priv struct Known {
  name : String
  shape : Shape
}

///|
/// The type inside `Array[T]`.
fn inner_array(ty : String) -> String? {
  let ty = ty.trim(chars=" ")
  if ty.has_prefix("Array[") && ty.has_suffix("]") {
    Some(
      ty
      .exact_view(start="Array[".length(), end=ty.length() - 1)
      .trim(chars=" ")
      .to_owned(),
    )
  } else {
    None
  }
}

///|
/// The type inside `T?` or `Option[T]`, or `None` when `ty` is not optional.
fn inner_option(ty : String) -> String? {
  let ty = ty.trim(chars=" ")
  if ty.has_suffix("?") {
    Some(ty.exact_view(end=ty.length() - 1).trim(chars=" ").to_owned())
  } else {
    inside(ty, "Option")
  }
}

///|
/// `T` out of `Name[T]`.
fn inside(ty : StringView, name : String) -> String? {
  let ty = ty.trim(chars=" ")
  let open = name + "["
  if ty.has_prefix(open) && ty.has_suffix("]") {
    Some(
      ty
      .exact_view(start=open.length(), end=ty.length() - 1)
      .trim(chars=" ")
      .to_owned(),
    )
  } else {
    None
  }
}

///|
/// The annotated output type at the core of `ty`, past `T?` and `Array[T]`:
/// what an argument or an input field cannot take.
fn output_core(ty : String, known : Map[String, Known]) -> String? {
  let base = inner_option(ty).unwrap_or(ty.trim(chars=" ").to_owned())
  match inside(base, "Array") {
    Some(item) => output_core(item, known)
    None =>
      match known.get(base) {
        Some({ shape: Object | Union | Interface, .. }) => Some(base)
        _ => None
      }
  }
}

///|
/// The scalar a MoonBit type stands for.
fn scalar_of(ty : String) -> String? {
  match ty {
    "String" => Some("String")
    "Int" | "Int32" => Some("Int")
    "Double" => Some("Float")
    "Bool" => Some("Boolean")
    "Int64" => Some("Int64")
    _ => None
  }
}

///|
fn builtin_scalar(name : String) -> Bool {
  name is ("String" | "Int" | "Float" | "Boolean" | "ID")
}

///|
/// The GraphQL type of a MoonBit type, as the `GqlType` it is written with:
/// `NonNull` unless `T?`, `ListOf` for `Array[T]`. `kind` names the innermost
/// type instead. Each custom scalar met lands in `scalars`. `None` for a type
/// that has no GraphQL counterpart.
fn graphql_type(
  ty : String,
  known : Map[String, Known],
  kind : String?,
  scalars : Map[String, Unit],
) -> String? {
  let (base, nullable) = match inner_option(ty) {
    Some(inner) => (inner, true)
    None => (ty.trim(chars=" ").to_owned(), false)
  }
  let named = match inside(base, "Array") {
    Some(item) =>
      match graphql_type(item, known, kind, scalars) {
        Some(t) => "ListOf(\{t})"
        None => return None
      }
    None => {
      let named = match kind {
        Some(k) => Some(k)
        None =>
          match known.get(base) {
            Some(k) => Some(k.name)
            None => scalar_of(base)
          }
      }
      guard named is Some(n) else { return None }
      if known.values().any(k => k.name == n) {
        "Named(\{quoted(n)})"
      } else {
        if !builtin_scalar(n) {
          scalars.set(n, ())
        }
        "Scalar(\{quoted(n)})"
      }
    }
  }
  Some(if nullable { named } else { "NonNull(\{named})" })
}

///|
/// `password_hash` as `passwordHash`; leading underscores stay.
fn camel(name : String) -> String {
  let out = StringBuilder()
  let mut up = false
  let mut lead = true
  for c in name {
    if c == '_' && !lead {
      up = true
    } else {
      out.write_char(if up { c.to_ascii_uppercase() } else { c })
      up = false
      lead = lead && c == '_'
    }
  }
  out.to_string()
}

///|
fn quoted(text : String) -> String {
  @quote.string(text).to_string()
}