open Stdppx
open Ppxlib
open Ast_builder.Default

let alphabet =
  Array.init
    (Char.code 'z' - Char.code 'a' + 1)
    ~f:(fun i -> String.make 1 (Char.chr (i + Char.code 'a')))

let vars_of_list ~get_loc l =
  List.mapi l ~f:(fun i x -> { txt = alphabet.(i); loc = get_loc x })

let evar_of_var { txt; loc } = evar ~loc txt
let pvar_of_var { txt; loc } = pvar ~loc txt
let tvar_of_var { txt; loc } = ptyp_var ~loc txt
let evars_of_vars = List.map ~f:evar_of_var
let pvars_of_vars = List.map ~f:pvar_of_var
let tvars_of_vars = List.map ~f:tvar_of_var

let fst_expr ~loc expr =
  match expr.pexp_desc with
  | Pexp_tuple [ fst; _ ] -> fst
  | _ -> [%expr Stdlib.fst [%e expr]]

let snd_expr ~loc expr =
  match expr.pexp_desc with
  | Pexp_tuple [ _; snd ] -> snd
  | _ -> [%expr Stdlib.snd [%e expr]]

let methods_of_class_exn = function
  | {
      pstr_desc =
        Pstr_class
          [
            {
              pci_expr = { pcl_desc = Pcl_structure { pcstr_fields = l; _ }; _ };
              _;
            };
          ];
      _;
    } ->
      l
  | _ -> assert false

module Backends = struct
  class reconstructors =
    object
      method record ~loc flds = pexp_record ~loc flds None

      method construct ~loc id args =
        pexp_construct ~loc id
          (match args with [] -> None | _ -> Some (pexp_tuple ~loc args))

      method tuple ~loc es = pexp_tuple ~loc es
    end

  class type what = object
    method name : string
    inherit reconstructors

    method class_params :
      loc:Location.t -> (core_type * (variance * injectivity)) list

    method virtual_methods : loc:Location.t -> class_field list
    method apply : loc:Location.t -> expression -> expression list -> expression
    method abstract : loc:Location.t -> pattern -> expression -> expression

    (* Basic combinator type *)
    method typ : loc:Location.t -> core_type -> core_type
    method any : loc:Location.t -> expression

    method combine :
      loc:Location.t ->
      (string loc * expression) list ->
      reconstruct:expression ->
      expression
  end

  let mapper : what =
    object
      method name = "map"
      inherit reconstructors
      method class_params ~loc:_ = []
      method virtual_methods ~loc:_ = []
      method apply ~loc expr args = eapply ~loc expr args
      method abstract ~loc patt expr = pexp_fun ~loc Nolabel None patt expr
      method typ ~loc ty = ptyp_arrow ~loc Nolabel ty ty
      method any ~loc = [%expr fun x -> x]

      method combine ~loc combinators ~reconstruct =
        List.fold_right combinators ~init:reconstruct ~f:(fun (v, expr) acc ->
            pexp_let ~loc Nonrecursive
              [ value_binding ~loc ~pat:(pvar_of_var v) ~expr ]
              acc)
    end

  let iterator : what =
    object
      method name = "iter"
      inherit reconstructors
      method class_params ~loc:_ = []
      method virtual_methods ~loc:_ = []
      method apply ~loc expr args = eapply ~loc expr args
      method abstract ~loc patt expr = pexp_fun ~loc Nolabel None patt expr
      method typ ~loc ty = [%type: [%t ty] -> unit]
      method any ~loc = [%expr fun _ -> ()]

      method combine ~loc combinators ~reconstruct:_ =
        match List.rev combinators with
        | [] -> [%expr ()]
        | (_, expr) :: rest ->
            List.fold_left rest ~init:expr ~f:(fun acc (_v, expr) ->
                pexp_sequence ~loc expr acc)
    end

  let folder : what =
    object
      method name = "fold"
      inherit reconstructors

      method class_params ~loc =
        [ (ptyp_var ~loc "acc", (NoVariance, NoInjectivity)) ]

      method virtual_methods ~loc:_ = []
      method apply ~loc expr args = eapply ~loc expr (args @ [ evar ~loc "acc" ])

      method abstract ~loc patt expr =
        eabstract ~loc [ patt; pvar ~loc "acc" ] expr

      method typ ~loc ty = [%type: [%t ty] -> 'acc -> 'acc]
      method any ~loc = [%expr fun _ acc -> acc]

      method combine ~loc combinators ~reconstruct:_ =
        match combinators with
        | [ (_, expr) ] -> expr
        | _ ->
            List.fold_right combinators ~init:[%expr acc]
              ~f:(fun (_v, expr) acc ->
                [%expr
                  let acc = [%e expr] in
                  [%e acc]])
    end

  let fold_mapper : what =
    object
      method name = "fold_map"
      inherit reconstructors

      method class_params ~loc =
        [ (ptyp_var ~loc "acc", (NoVariance, NoInjectivity)) ]

      method virtual_methods ~loc:_ = []
      method apply ~loc expr args = eapply ~loc expr (args @ [ evar ~loc "acc" ])

      method abstract ~loc patt expr =
        eabstract ~loc [ patt; pvar ~loc "acc" ] expr

      method typ ~loc ty = [%type: [%t ty] -> 'acc -> [%t ty] * 'acc]
      method any ~loc = [%expr fun x acc -> (x, acc)]

      method combine ~loc combinators ~reconstruct =
        List.fold_right combinators
          ~init:[%expr [%e reconstruct], acc]
          ~f:(fun (v, expr) acc ->
            [%expr
              let [%p pvar_of_var v], acc = [%e expr] in
              [%e acc]])
    end

  exception Found

  let uses_var var =
    let iter =
      object
        inherit Ast_traverse.iter as super

        method! expression_desc =
          function
          | Pexp_ident { txt = Lident id; _ } when String.equal id var ->
              raise_notrace Found
          | e -> super#expression_desc e
      end
    in
    fun e ->
      try
        iter#expression e;
        false
      with Found -> true

  let mapper_with_context : what =
    let uses_ctx = uses_var "ctx" in
    object
      method name = "map_with_context"
      inherit reconstructors

      method class_params ~loc =
        [ (ptyp_var ~loc "ctx", (NoVariance, NoInjectivity)) ]

      method virtual_methods ~loc:_ = []
      method apply ~loc expr args = eapply ~loc expr (evar ~loc "ctx" :: args)

      method abstract ~loc patt expr =
        if uses_ctx expr then eabstract ~loc [ pvar ~loc "ctx"; patt ] expr
        else eabstract ~loc [ pvar ~loc "_ctx"; patt ] expr

      method typ ~loc ty = [%type: 'ctx -> [%t ty] -> [%t ty]]
      method any ~loc = [%expr fun _ctx x -> x]

      method combine ~loc combinators ~reconstruct =
        List.fold_right combinators ~init:reconstruct ~f:(fun (v, expr) acc ->
            [%expr
              let [%p pvar_of_var v] = [%e expr] in
              [%e acc]])
    end

  let string_of_lid id = String.concat ~sep:"." (Longident.flatten_exn id)

  let lifter : what =
    object
      method name = "lift"

      method class_params ~loc =
        [ (ptyp_var ~loc "res", (NoVariance, NoInjectivity)) ]

      method virtual_methods ~loc =
        methods_of_class_exn
          [%stri
            class virtual blah =
              object
                method virtual record : (string * 'res) list -> 'res
                method virtual constr : string -> 'res list -> 'res
                method virtual tuple : 'res list -> 'res
                method virtual other : 'a. 'a -> 'res
              end]

      method apply ~loc expr args = eapply ~loc expr args
      method abstract ~loc patt expr = pexp_fun ~loc Nolabel None patt expr
      method typ ~loc ty = [%type: [%t ty] -> 'res]
      method any ~loc = [%expr self#other]

      method combine ~loc combinators ~reconstruct =
        List.fold_right combinators ~init:reconstruct ~f:(fun (v, expr) acc ->
            pexp_let ~loc Nonrecursive
              [ value_binding ~loc ~pat:(pvar_of_var v) ~expr ]
              acc)

      method record ~loc flds =
        let flds =
          elist ~loc
            (List.map flds ~f:(fun (lab, e) ->
                 pexp_tuple
                   ~loc:{ lab.loc with loc_end = e.pexp_loc.loc_end }
                   [ estring ~loc:lab.loc (string_of_lid lab.txt); e ]))
        in
        [%expr self#record [%e flds]]

      method construct ~loc id args =
        let args = elist ~loc args in
        [%expr
          self#constr [%e estring ~loc:id.loc (string_of_lid id.txt)] [%e args]]

      method tuple ~loc es = [%expr self#tuple [%e elist ~loc es]]
    end

  let lift_mapper_with_context : what =
    let uses_ctx = uses_var "ctx" in
    object
      method name = "lift_map_with_context"

      method class_params ~loc =
        [
          (ptyp_var ~loc "ctx", (NoVariance, NoInjectivity));
          (ptyp_var ~loc "res", (NoVariance, NoInjectivity));
        ]

      method virtual_methods ~loc =
        methods_of_class_exn
          [%stri
            class virtual blah =
              object
                method virtual record : 'ctx -> (string * 'res) list -> 'res
                method virtual constr : 'ctx -> string -> 'res list -> 'res
                method virtual tuple : 'ctx -> 'res list -> 'res
                method virtual other : 'a. 'ctx -> 'a -> 'res
              end]

      method apply ~loc expr args = eapply ~loc expr (evar ~loc "ctx" :: args)

      method abstract ~loc patt expr =
        let ctx_pat =
          if uses_ctx expr then pvar ~loc "ctx" else pvar ~loc "_ctx"
        in
        eabstract ~loc [ ctx_pat; patt ] expr

      method typ ~loc ty = [%type: 'ctx -> [%t ty] -> [%t ty] * 'res]
      method any ~loc = [%expr fun ctx x -> (x, self#other ctx x)]

      method combine ~loc combinators ~reconstruct =
        List.fold_right combinators ~init:reconstruct ~f:(fun (v, expr) acc ->
            pexp_let ~loc Nonrecursive
              [ value_binding ~loc ~pat:(pvar_of_var v) ~expr ]
              acc)

      method record ~loc flds =
        let record =
          pexp_record ~loc
            (List.map flds ~f:(fun (lab, e) -> (lab, fst_expr ~loc e)))
            None
        in
        let flds =
          elist ~loc
            (List.map flds ~f:(fun (lab, e) ->
                 pexp_tuple
                   ~loc:{ lab.loc with loc_end = e.pexp_loc.loc_end }
                   [
                     estring ~loc:lab.loc (string_of_lid lab.txt);
                     snd_expr ~loc e;
                   ]))
        in
        [%expr [%e record], self#record ctx [%e flds]]

      method construct ~loc id args =
        let constr =
          pexp_construct ~loc id
            (pexp_tuple_opt ~loc (List.map args ~f:(fst_expr ~loc)))
        in
        let res =
          let args = elist ~loc (List.map args ~f:(snd_expr ~loc)) in
          [%expr
            self#constr ctx
              [%e estring ~loc:id.loc (string_of_lid id.txt)]
              [%e args]]
        in
        [%expr [%e constr], [%e res]]

      method tuple ~loc es =
        let tuple = pexp_tuple ~loc (List.map es ~f:(fst_expr ~loc)) in
        let res =
          [%expr
            self#tuple ctx [%e elist ~loc (List.map es ~f:(snd_expr ~loc))]]
        in
        [%expr [%e tuple], [%e res]]
    end

  let all =
    [
      mapper;
      iterator;
      folder;
      fold_mapper;
      mapper_with_context;
      lifter;
      lift_mapper_with_context;
    ]
end

type what = Backends.what

let mapper_type ~(what : what) ~loc type_name params =
  let vars = vars_of_list params ~get_loc:(fun t -> t.ptyp_loc) in
  let params = tvars_of_vars vars in
  let ty = ptyp_constr ~loc type_name params in
  let ty =
    List.fold_right params ~init:(what#typ ~loc ty) ~f:(fun param ty ->
        let loc = param.ptyp_loc in
        ptyp_arrow ~loc Nolabel (what#typ ~loc param) ty)
  in
  ptyp_poly ~loc vars ty

let constrained_mapper ~(what : what) ?(is_gadt = false) mapper td =
  let vars = vars_of_list td.ptype_params ~get_loc:(fun (t, _) -> t.ptyp_loc) in
  let make_type params =
    let loc = td.ptype_loc in
    let ty = ptyp_constr ~loc (Loc.map td.ptype_name ~f:lident) params in
    List.fold_right params ~init:(what#typ ~loc:td.ptype_loc ty)
      ~f:(fun param ty ->
        let loc = param.ptyp_loc in
        ptyp_arrow ~loc Nolabel (what#typ ~loc param) ty)
  in
  let typ =
    let loc = td.ptype_loc in
    ptyp_poly ~loc vars (make_type (tvars_of_vars vars))
  in
  let mapper =
    if false || is_gadt then
      let typs =
        List.map vars ~f:(fun v ->
            ptyp_constr ~loc:v.loc (Loc.map v ~f:lident) [])
      in
      List.fold_right vars
        ~init:(pexp_constraint ~loc:mapper.pexp_loc mapper (make_type typs))
        ~f:(fun v e -> pexp_newtype ~loc:v.loc v e)
    else mapper
  in
  pexp_poly ~loc:mapper.pexp_loc mapper (Some typ)

let mangle_type_name lid =
  let rec mangled_parts lid ~suffix =
    match lid with
    | Lident s -> String.lowercase_ascii s :: suffix
    | Ldot (lid, s) ->
        mangled_parts lid ~suffix:("__" :: String.lowercase_ascii s :: suffix)
    | Lapply (a, b) ->
        mangled_parts a ~suffix:("_'" :: mangled_parts b ~suffix:("'" :: suffix))
  in
  mangled_parts lid ~suffix:[] |> String.concat ~sep:""

let rec type_expr_mapper ~(what : what) te =
  let loc = te.ptyp_loc in
  match te.ptyp_desc with
  | Ptyp_var s -> evar ~loc ("_" ^ s)
  | Ptyp_tuple tes ->
      let vars = vars_of_list tes ~get_loc:(fun t -> t.ptyp_loc) in
      let deconstruct = ppat_tuple ~loc (pvars_of_vars vars) in
      let reconstruct = what#tuple ~loc (evars_of_vars vars) in
      let mappers = map_variables ~what vars tes in
      what#abstract ~loc deconstruct (what#combine ~loc mappers ~reconstruct)
  | Ptyp_constr (path, params) -> (
      let map =
        pexp_send ~loc (evar ~loc "self") (Loc.map path ~f:mangle_type_name)
      in
      match params with
      | [] -> map
      | _ ->
          eapply ~loc map
            (List.map params ~f:(fun te -> type_expr_mapper ~what te)))
  | _ -> what#any ~loc

and map_variables ~(what : what) vars tes =
  List.map2 tes vars ~f:(fun te var ->
      ( var,
        what#apply ~loc:te.ptyp_loc
          (type_expr_mapper ~what te)
          [ evar_of_var var ] ))

let gen_record' ~(what : what) ~loc lds =
  let vars = List.map lds ~f:(fun ld -> ld.pld_name) in
  let deconstruct =
    ppat_record ~loc
      (List.map vars ~f:(fun v -> (Loc.map v ~f:lident, pvar_of_var v)))
      Closed
  in
  let reconstruct =
    what#record ~loc
      (List.map vars ~f:(fun v -> (Loc.map v ~f:lident, evar_of_var v)))
  in
  let mappers =
    map_variables ~what vars (List.map lds ~f:(fun ld -> ld.pld_type))
  in
  (deconstruct, reconstruct, mappers)

let gen_record ~(what : what) ~loc lds =
  let deconstruct, reconstruct, mappers = gen_record' ~what lds ~loc in
  what#abstract ~loc deconstruct (what#combine ~loc mappers ~reconstruct)

let is_constant_constructor cd =
  match cd.pcd_args with Pcstr_tuple [] -> true | _ -> false

let erase_type_variables =
  object
    inherit Ast_traverse.map as super

    method! core_type_desc =
      function Ptyp_var _ -> Ptyp_any | x -> super#core_type_desc x
  end

let gen_variant ~(what : what) ~loc cds =
  if
    String.( <> ) what#name "lift"
    && List.for_all cds ~f:is_constant_constructor
  then what#any ~loc
  else
    let cases =
      List.map cds ~f:(fun cd ->
          let cstr = Loc.map cd.pcd_name ~f:lident in
          let loc = cd.pcd_loc in
          let args =
            match cd.pcd_res with
            | None -> cd.pcd_args
            | Some _ ->
                (* This is a big sur-approximation but it's enough for our only use of GADTs
                   in ppx_custom_format *)
                erase_type_variables#constructor_arguments cd.pcd_args
          in
          match args with
          | Pcstr_tuple args ->
              let vars = vars_of_list args ~get_loc:(fun t -> t.ptyp_loc) in
              let deconstruct =
                ppat_construct cstr ~loc
                  (match vars with
                  | [] -> None
                  | _ -> Some (ppat_tuple ~loc (pvars_of_vars vars)))
              in
              let reconstruct = what#construct cstr ~loc (evars_of_vars vars) in
              let mappers = map_variables ~what vars args in
              case ~lhs:deconstruct
                ~rhs:(what#combine ~loc mappers ~reconstruct)
                ~guard:None
          | Pcstr_record labels ->
              let deconstruct, reconstruct, mappers =
                gen_record' ~loc ~what labels
              in
              let deconstruct = ppat_construct ~loc cstr (Some deconstruct) in
              let reconstruct = what#construct ~loc cstr [ reconstruct ] in
              case ~lhs:deconstruct
                ~rhs:(what#combine ~loc mappers ~reconstruct)
                ~guard:None)
    in
    what#abstract ~loc (pvar ~loc "x") (pexp_match ~loc (evar ~loc "x") cases)

let gen_mapper ~(what : what) td =
  let body =
    let loc = td.ptype_loc in
    match td.ptype_kind with
    | Ptype_open -> what#any ~loc
    | Ptype_record lds -> gen_record ~what lds ~loc
    | Ptype_variant cds -> gen_variant ~what cds ~loc
    | Ptype_abstract -> (
        match td.ptype_manifest with
        | None -> what#any ~loc
        | Some te -> type_expr_mapper ~what te)
  in
  List.fold_right td.ptype_params ~init:body ~f:(fun (ty, _) acc ->
      let loc = ty.ptyp_loc in
      match ty.ptyp_desc with
      | Ptyp_var s -> pexp_fun ~loc Nolabel None (pvar ~loc ("_" ^ s)) acc
      | _ -> pexp_fun ~loc Nolabel None (ppat_any ~loc) acc)

let type_deps =
  let collect =
    object
      inherit [int Longident.Map.t] Ast_traverse.fold as super

      method! core_type t acc =
        let acc =
          match t.ptyp_desc with
          | Ptyp_constr (id, vars) ->
              Longident.Map.add id.txt (List.length vars) acc
          | _ -> acc
        in
        super#core_type t acc
    end
  in
  fun tds ->
    let empty = Longident.Map.empty in
    let map =
      List.fold_left tds ~init:empty ~f:(fun map td ->
          let map = collect#type_kind td.ptype_kind map in
          match (td.ptype_kind, td.ptype_manifest) with
          | Ptype_abstract, Some ty -> collect#core_type ty map
          | _ -> map)
    in
    let map =
      List.fold_left tds ~init:map ~f:(fun map td ->
          Longident.Map.remove (Lident td.ptype_name.txt) map)
    in
    Longident.Map.bindings map

let filter_virtual_methods ~methods ~virtual_methods =
  let collect =
    object
      inherit [String.Set.t] Ast_traverse.fold as super

      method! expression_desc x acc =
        match x with
        | Pexp_send
            ( _,
              { txt = ("tuple" | "record" | "constr" | "other") as s; loc = _ }
            ) ->
            String.Set.add s acc
        | _ -> super#expression_desc x acc
    end
  in
  let used = collect#list collect#class_field methods String.Set.empty in
  List.filter virtual_methods ~f:(fun m ->
      match m.pcf_desc with
      | Pcf_method (s, _, _) -> String.Set.mem s.txt used
      | _ -> false)

let gen_class ~(what : what) ~loc tds =
  let class_params = what#class_params ~loc in
  let virtual_methods =
    List.map (type_deps tds) ~f:(fun (id, arity) ->
        pcf_method ~loc
          ( { txt = mangle_type_name id; loc },
            Public,
            Cfk_virtual
              (mapper_type ~what ~loc { txt = id; loc }
                 (List.init ~len:arity ~f:(fun _ -> ptyp_any ~loc))) ))
  in
  let methods =
    List.map tds ~f:(fun td ->
        let loc = td.ptype_loc in
        let mapper = gen_mapper ~what td in
        let is_gadt =
          match td.ptype_kind with
          | Ptype_variant cds ->
              List.exists cds ~f:(fun cd -> Option.is_some cd.pcd_res)
          | _ -> false
        in
        let mapper = constrained_mapper ~what ~is_gadt mapper td in
        pcf_method ~loc (td.ptype_name, Public, Cfk_concrete (Fresh, mapper)))
  in
  let virtual_methods =
    filter_virtual_methods ~methods ~virtual_methods:(what#virtual_methods ~loc)
    @ virtual_methods
  in
  let virt = if List.is_empty virtual_methods then Concrete else Virtual in
  class_infos ~loc ~virt ~params:class_params ~name:{ loc; txt = what#name }
    ~expr:
      (pcl_structure ~loc
         (class_structure
            ~self:(ppat_var ~loc { txt = "self"; loc })
            ~fields:(virtual_methods @ methods)))

let gen_str ~(what : what) ~loc ~path:_ (rf, tds) =
  (match rf with
  | Nonrecursive ->
      (* The method name would clash... *)
      Location.raise_errorf ~loc "ppxlib_traverse doesn't support nonrec"
  | Recursive -> ());
  let cl = gen_class ~loc ~what tds in
  [ pstr_class ~loc:cl.pci_loc [ cl ] ]

let () =
  let derivers =
    List.map Backends.all ~f:(fun what ->
        Deriving.add ("traverse_" ^ what#name)
          ~str_type_decl:(Deriving.Generator.make_noarg (gen_str ~what)))
  in
  Deriving.add_alias "traverse" (List.rev derivers) |> Deriving.ignore
