// Turning parsed MoonBit source into `Target`s.
//
// Only attributes qualified with `serde.` are considered. The compiler ignores
// unknown namespaced attributes, so an unqualified `#derive(...)` or another
// tool's `#gobject.subclass` passes straight through untouched.

///|
const NAMESPACE : String = "serde"

///|
/// Splits a `serde.`-qualified attribute into its name and arguments.
///
/// `#serde.skip` is `("skip", [])` and `#serde.rename("why")` is
/// `("rename", [String("why")])`. Anything in another namespace is `None`.
fn attribute_call(
  attr : @attribute.Attribute,
) -> (String, Array[@attribute.Expr])? {
  match attr.parsed {
    Some(Apply(id, props)) =>
      if id.qual == Some(NAMESPACE) {
        let args = []
        for prop in props {
          match prop {
            Expr(e) => args.push(e)
            Labeled(_, e) => args.push(e)
          }
        }
        Some((id.name, args))
      } else {
        None
      }
    Some(Ident(id)) =>
      if id.qual == Some(NAMESPACE) {
        Some((id.name, []))
      } else {
        None
      }
    _ => None
  }
}

///|
/// Whether an attribute is in our namespace at all, judged from the raw text.
///
/// `attribute_call` only recognises the forms this tool understands, so
/// something like `#serde.skip(1)` — whose argument the attribute grammar has
/// no representation for — would otherwise parse to nothing and be silently
/// ignored. Anything spelled `#serde.*` has to be accounted for.
fn is_ours(attr : @attribute.Attribute) -> Bool {
  attr.raw.has_prefix("#" + NAMESPACE + ".")
}

///|
fn[T] unparseable(
  attr : @attribute.Attribute,
  where_ : String,
) -> T raise DeriveError {
  raise DeriveError(
    where_~,
    message="cannot make sense of attribute `\{attr.raw}`",
  )
}

///|
fn expr_ident(expr : @attribute.Expr) -> String? {
  match expr {
    Ident(id) => Some(id.name)
    _ => None
  }
}

///|
fn expr_string(expr : @attribute.Expr) -> String? {
  match expr {
    String(text) => Some(text)
    _ => None
  }
}

///|
/// Reads `#serde.rename("...")` and `#serde.skip` off a field or variant.
fn read_member_attributes(
  attrs : @list.List[@attribute.Attribute],
  where_ : String,
  ident : String,
) -> (String, Bool) raise DeriveError {
  let mut name = ident
  let mut skip = false
  for attr in attrs {
    guard attribute_call(attr) is Some((call, args)) else {
      if is_ours(attr) {
        unparseable(attr, where_)
      }
      continue
    }
    match call {
      "rename" => {
        guard args.length() == 1 && expr_string(args[0]) is Some(text) else {
          raise DeriveError(
            where_~,
            message="#serde.rename on `\{ident}` takes exactly one string",
          )
        }
        name = text
      }
      "skip" => {
        guard args.is_empty() else {
          raise DeriveError(
            where_~,
            message="#serde.skip on `\{ident}` takes no arguments",
          )
        }
        skip = true
      }
      other =>
        raise DeriveError(
          where_~,
          message="unknown attribute `#serde.\{other}` on `\{ident}`",
        )
    }
  }
  (name, skip)
}

///|
fn scan_fields(
  fields : @list.List[@syntax.FieldDecl],
  where_ : String,
) -> Array[Field] raise DeriveError {
  let out = []
  for field in fields {
    let ident = field.name.label
    let (name, skip) = read_member_attributes(field.attrs, where_, ident)
    out.push({ ident, name, optional: field.ty is Option(..), skip, })
  }
  for index, field in out {
    if field.skip {
      continue
    }
    for previous in 0.. Case raise DeriveError {
  let ident = constr.name.name
  let (name, skip) = read_member_attributes(constr.attrs, where_, ident)
  guard !skip else {
    raise DeriveError(
      where_~,
      message="#serde.skip is not supported on enum variants (`\{ident}`)",
    )
  }
  let args = match constr.args {
    None => []
    Some(list) => list.to_array()
  }
  if args.is_empty() {
    return { ident, name, payload: Unit, }
  }
  let labelled = args.filter(arg => arg.label is Some(_)).length()
  if labelled == 0 {
    let payload = if args.length() == 1 {
      Newtype
    } else {
      Tuple(args.length())
    }
    return { ident, name, payload, }
  }
  guard labelled == args.length() else {
    raise DeriveError(
      where_~,
      message="variant `\{ident}` mixes labelled and positional payloads, which has no data model equivalent",
    )
  }
  let fields = []
  for arg in args {
    guard arg.label is Some(label) else { continue }
    fields.push({
      ident: label.name,
      name: label.name,
      optional: false,
      skip: false,
    })
  }
  { ident, name, payload: Struct(fields), }
}

///|
/// Extracts every `#serde.derive(...)` target from one parsed file.
///
/// `where_` is used verbatim as the location prefix of any `DeriveError`.
fn scan(
  impls : @list.List[@syntax.Impl],
  where_ : String,
) -> Array[Target] raise DeriveError {
  let targets = []
  for impl_ in impls {
    guard impl_ is TopTypeDef(decl) else { continue }
    let mut serialize = false
    let mut deserialize = false
    let mut marked = false
    for attr in decl.attrs {
      guard attribute_call(attr) is Some((call, args)) else {
        if is_ours(attr) {
          unparseable(attr, where_)
        }
        continue
      }
      guard call == "derive" else {
        raise DeriveError(
          where_~,
          message="unknown attribute `#serde.\{call}` on type `\{decl.tycon}`",
        )
      }
      marked = true
      guard !args.is_empty() else {
        raise DeriveError(
          where_~,
          message="#serde.derive on `\{decl.tycon}` names no traits",
        )
      }
      for arg in args {
        match expr_ident(arg) {
          Some("Serialize") => serialize = true
          Some("Deserialize") => deserialize = true
          Some(other) =>
            raise DeriveError(
              where_~,
              message="cannot derive `\{other}` for `\{decl.tycon}`; expected Serialize or Deserialize",
            )
          None =>
            raise DeriveError(
              where_~,
              message="#serde.derive on `\{decl.tycon}` takes trait names, not literals",
            )
        }
      }
    }
    guard marked else { continue }
    let site = "\{where_}: type `\{decl.tycon}`"
    let shape = match decl.components {
      Record(fields~, ..) =>
        if fields.is_empty() {
          UnitStruct
        } else {
          Record(scan_fields(fields, site))
        }
      TupleStruct(types) =>
        if types.length() == 1 {
          NewtypeStruct
        } else {
          TupleStruct(types.length())
        }
      Variant(constrs) => {
        let cases = []
        for constr in constrs {
          cases.push(scan_case(constr, site))
        }
        Variant(cases)
      }
      _ =>
        raise DeriveError(
          where_~,
          message="`\{decl.tycon}` is not a struct or enum, so there is nothing to derive",
        )
    }
    let params = []
    for binder in decl.params {
      match binder.name {
        Some(name) => params.push(name)
        None =>
          raise DeriveError(
            where_~,
            message="`\{decl.tycon}` has an unnamed type parameter",
          )
      }
    }
    targets.push({
      name: decl.tycon,
      params,
      public: decl.type_vis is Pub(..),
      shape,
      serialize,
      deserialize,
    })
  }
  targets
}