open Ppxlib
open Ast_builder.Default
module E = Extension
module A = Ast_pattern

type quoted_attributes = {
  quoted_attributes : attributes;
      (* The attributes that appear quoted, e.g. [@foo] in [%expr [%e e] [@foo]] *)
  field_name : string;
      (* The field name where attributes are stored for the kind of AST the quoted
         attributes are placed on, e.g. pexp_attributes. *)
}

let coalesce_arity_expr (input : expression) super =
  match input with
  | { pexp_desc = Pexp_function _; pexp_loc = loc; _ } ->
      let ppxlib_coalesce_arity =
        Ldot
          ( Ldot (Ldot (Lident "Ppxlib", "Ast_builder"), "Default"),
            "coalesce_arity" )
      in
      pexp_apply ~loc
        (pexp_ident ~loc { txt = ppxlib_coalesce_arity; loc })
        [ (Nolabel, super input) ]
  | exp -> super exp

(* Produce a pattern that matches on the maximum arity of a function *)
let coalesce_arity_pat (input : expression) super =
  match input with
  | { pexp_desc = Pexp_function _; pexp_loc = _loc; _ } as exp ->
      let c = Ast_builder.Default.coalesce_arity exp in
      super c
  | e -> super e

module Make (M : sig
  type result

  val annotate : result -> core_type -> result

  val cast :
    (* The instance of the [std_lifters] class being used. *)
    < attributes : attributes -> result
    ; typed : result -> string -> result
    ; .. > ->
    extension ->
    quoted_attributes option ->
    (* e.g. [expression]; the callee is responsible for calling
       [self#typed ast type_name] on the AST to add a type annotation
       that constrains its type.
    *)
    type_name:string ->
    result

  val location : location -> result
  val location_stack : (location -> result) option
  val attributes : (location -> result) option
  val coalesce : (expression -> (expression -> result) -> result) option

  class std_lifters : location -> [result] Ppxlib_traverse_builtins.std_lifters
end) =
struct
  let lift loc =
    object (self)
      inherit [M.result] Ast_traverse.lift as super
      inherit! M.std_lifters loc

      method typed ast name =
        let loc = { loc with loc_ghost = true } in
        M.annotate ast
          (ptyp_constr ~loc
             { loc; txt = Ldot (Ldot (Lident "Ppxlib_ast", "Ast"), name) }
             [])

      method! attribute x =
        Attribute.mark_as_handled_manually x;
        super#attribute x

      method! location _ = M.location loc

      method! attributes x =
        match M.attributes with
        | None -> super#attributes x
        | Some f ->
            assert_no_attributes x;
            f loc

      method! location_stack x =
        match M.location_stack with
        | None -> super#location_stack x
        | Some f -> f loc

      method! expression e =
        match (M.coalesce, e.pexp_desc) with
        | _, Pexp_extension (({ txt = "e"; _ }, _) as ext) ->
            let attributes =
              {
                quoted_attributes = e.pexp_attributes;
                field_name = "pexp_attributes";
              }
            in
            M.cast self ext (Some attributes) ~type_name:"expression"
        | Some f, _ -> f e super#expression
        | None, _ -> super#expression e

      method! pattern p =
        match p.ppat_desc with
        | Ppat_extension (({ txt = "p"; _ }, _) as ext) ->
            let attributes =
              {
                quoted_attributes = p.ppat_attributes;
                field_name = "ppat_attributes";
              }
            in
            M.cast self ext (Some attributes) ~type_name:"pattern"
        | _ -> super#pattern p

      method! core_type t =
        match t.ptyp_desc with
        | Ptyp_extension (({ txt = "t"; _ }, _) as ext) ->
            let attributes =
              {
                quoted_attributes = t.ptyp_attributes;
                field_name = "ptyp_attributes";
              }
            in
            M.cast self ext (Some attributes) ~type_name:"core_type"
        | _ -> super#core_type t

      method! module_expr m =
        match m.pmod_desc with
        | Pmod_extension (({ txt = "m"; _ }, _) as ext) ->
            let attributes =
              {
                quoted_attributes = m.pmod_attributes;
                field_name = "pmod_attributes";
              }
            in
            M.cast self ext (Some attributes) ~type_name:"module_expr"
        | _ -> super#module_expr m

      method! module_type m =
        match m.pmty_desc with
        | Pmty_extension (({ txt = "m"; _ }, _) as ext) ->
            let attributes =
              {
                quoted_attributes = m.pmty_attributes;
                field_name = "pmty_attributes";
              }
            in
            M.cast self ext (Some attributes) ~type_name:"module_type"
        | _ -> super#module_type m

      method! structure_item i =
        match i.pstr_desc with
        | Pstr_extension ((({ txt = "i"; _ }, _) as ext), attrs) ->
            assert_no_attributes attrs;
            M.cast self ext None ~type_name:"structure_item"
        | _ -> super#structure_item i

      method! signature_item i =
        match i.psig_desc with
        | Psig_extension ((({ txt = "i"; _ }, _) as ext), attrs) ->
            assert_no_attributes attrs;
            M.cast self ext None ~type_name:"signature_item"
        | _ -> super#signature_item i
    end
end

module Expr = Make (struct
  type result = expression

  let location loc = evar ~loc:{ loc with loc_ghost = true } "loc"
  let location_stack = None
  let attributes = None
  let coalesce = Some coalesce_arity_expr

  class std_lifters = Ppxlib_metaquot_lifters.expression_lifters

  let annotate e core_type = pexp_constraint ~loc:core_type.ptyp_loc e core_type

  (* Append the quoted attributes to the attributes present on the
     antiquoted construct. Take this as example:

     [%expr [%e e] [@attr]]

     Suppose e has pexp_attributes = [attr1]. Then the resulting attributes
     are [ attr1; [@attr] ]. The decision to put outer attributes (here,
     [@attr]) at the end of the list is consistent with other parts of ppxlib
     that accumulate attributes.
  *)
  let add_quoted_attributes self e { quoted_attributes; field_name } ~type_name
      ~loc =
    match quoted_attributes with
    | [] -> self#typed e type_name
    | _ :: _ ->
        let loc = { loc with loc_ghost = true } in
        let var = gen_symbol ~prefix:"_ppx_metaquot_helper_var" () in
        let var_expr = pexp_ident ~loc (Located.mk ~loc (Lident var)) in
        let field_name = Located.mk ~loc (Lident field_name) in
        let reified_attrs = self#attributes quoted_attributes in
        (* append arg1 arg2 = [%expr Stdlib.List.append [%e arg1] [%e arg2]] *)
        let append arg1 arg2 =
          pexp_apply ~loc
            (pexp_ident ~loc
               (Located.mk ~loc
                  (Ldot (Ldot (Lident "Stdlib", "List"), "append"))))
            [ (Nolabel, arg1); (Nolabel, arg2) ]
        in
        (*
         Morally,
         {[
           let var = ([%expr e] : [%type: type_name]) in
           { var
             with pexp_attributes = var.pexp_attributes @ [%e reified_attrs ]
           }
         ]}
        *)
        pexp_let Nonrecursive ~loc
          [
            value_binding ~loc
              ~pat:(ppat_var ~loc (Located.mk ~loc var))
              ~expr:(self#typed e type_name);
          ]
          (pexp_record ~loc
             [
               ( field_name,
                 append (pexp_field ~loc var_expr field_name) reified_attrs );
             ]
             (Some var_expr))

  let cast self ext attrs ~type_name =
    match snd ext with
    | PStr [ { pstr_desc = Pstr_eval (e, inner_attrs); _ } ] -> (
        assert_no_attributes inner_attrs;
        match attrs with
        | None -> self#typed e type_name
        | Some quoted_attrs ->
            add_quoted_attributes self e quoted_attrs ~type_name
              ~loc:(loc_of_extension ext))
    | _ ->
        pexp_extension ~loc:(loc_of_extension ext)
          (Location.error_extensionf ~loc:(loc_of_extension ext)
             "expression expected")
end)

module Patt = Make (struct
  type result = pattern

  let location loc = ppat_any ~loc:{ loc with loc_ghost = true }

  let location_stack =
    Some (fun loc -> ppat_any ~loc:{ loc with loc_ghost = true })

  let attributes = Some (fun loc -> ppat_any ~loc:{ loc with loc_ghost = true })
  let coalesce = Some coalesce_arity_pat

  class std_lifters = Ppxlib_metaquot_lifters.pattern_lifters

  let annotate p core_type = ppat_constraint ~loc:core_type.ptyp_loc p core_type

  let cast self ext attrs ~type_name =
    match snd ext with
    | PPat (p, None) ->
        (match attrs with
        | None -> ()
        | Some { quoted_attributes; field_name = _ } ->
            assert_no_attributes quoted_attributes);
        self#typed p type_name
    | PPat (_, Some e) ->
        Ast_builder.Default.(
          ppat_extension ~loc:e.pexp_loc
            (Location.error_extensionf ~loc:e.pexp_loc "guard not expected here"))
    | _ ->
        Ast_builder.Default.(
          ppat_extension ~loc:(loc_of_extension ext)
            (Location.error_extensionf ~loc:(loc_of_extension ext)
               "pattern expected"))
end)

let () =
  let extensions ctx lifter =
    [
      E.declare "metaquot.expr" ctx
        A.(single_expr_payload __)
        (fun ~loc ~path:_ e ->
          let lift = lifter loc in
          lift#typed (lift#expression e) "expression");
      E.declare "metaquot.pat" ctx
        A.(ppat __ none)
        (fun ~loc ~path:_ p ->
          let lift = lifter loc in
          lift#typed (lift#pattern p) "pattern");
      E.declare "metaquot.str" ctx
        A.(pstr __)
        (fun ~loc ~path:_ s ->
          let lift = lifter loc in
          lift#typed (lift#structure s) "structure");
      E.declare "metaquot.stri" ctx
        A.(pstr (__ ^:: nil))
        (fun ~loc ~path:_ s ->
          let lift = lifter loc in
          lift#typed (lift#structure_item s) "structure_item");
      E.declare "metaquot.sig" ctx
        A.(psig __)
        (fun ~loc ~path:_ s ->
          let lift = lifter loc in
          lift#typed (lift#signature s) "signature");
      E.declare "metaquot.sigi" ctx
        A.(psig (__ ^:: nil))
        (fun ~loc ~path:_ s ->
          let lift = lifter loc in
          lift#typed (lift#signature_item s) "signature_item");
      E.declare "metaquot.type" ctx
        A.(ptyp __)
        (fun ~loc ~path:_ t ->
          let lift = lifter loc in
          lift#typed (lift#core_type t) "core_type");
    ]
  in
  let extensions =
    extensions Expression Expr.lift @ extensions Pattern Patt.lift
  in
  Driver.register_transformation "metaquot" ~extensions
