(*
 * Copyright (c) 2013-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *)

open! IStd
open PolyVariantEqual

(** Functions for transformations of ast nodes *)

module L = Logging

type qual_type_to_sil_type = Tenv.t -> Clang_ast_t.qual_type -> Typ.t

type procname_from_decl =
     ?tenv:Tenv.t
  -> ?block_return_type:Clang_ast_t.qual_type
  -> ?outer_proc:Typ.Procname.t
  -> Clang_ast_t.decl
  -> Typ.Procname.t

let sanitize_name s = Str.global_replace (Str.regexp "[/ ]") "_" s

let get_qual_name qual_name_list =
  List.map ~f:sanitize_name qual_name_list |> QualifiedCppName.of_rev_list


let get_qualified_name ?(linters_mode = false) name_info =
  if not linters_mode then get_qual_name name_info.Clang_ast_t.ni_qual_name
  else
    (* Because we are in linters mode, we can't get precise info about templates,
        so we strip the template characters to not upset invariants in the system. *)
    let replace_template_chars qual_name =
      String.tr ~target:'<' ~replacement:'_' qual_name |> String.tr ~target:'>' ~replacement:'_'
    in
    let qual_names = List.map ~f:replace_template_chars name_info.Clang_ast_t.ni_qual_name in
    get_qual_name qual_names


let get_unqualified_name name_info =
  let name =
    match name_info.Clang_ast_t.ni_qual_name with
    | name :: _ ->
        name
    | [] ->
        name_info.Clang_ast_t.ni_name
  in
  sanitize_name name


let get_class_name_from_member member_name_info =
  match member_name_info.Clang_ast_t.ni_qual_name with
  | _ :: class_qual_list ->
      get_qual_name class_qual_list
  | [] ->
      assert false


let pointer_counter = ref 0

let get_fresh_pointer () =
  pointer_counter := !pointer_counter + 1 ;
  let internal_pointer = - !pointer_counter in
  internal_pointer


let dummy_source_range () =
  let dummy_source_loc = {Clang_ast_t.sl_file= None; sl_line= None; sl_column= None} in
  (dummy_source_loc, dummy_source_loc)


let dummy_stmt_info () =
  {Clang_ast_t.si_pointer= get_fresh_pointer (); si_source_range= dummy_source_range ()}


let named_decl_info_equal ndi1 ndi2 =
  match
    List.for_all2 ndi1.Clang_ast_t.ni_qual_name ndi2.Clang_ast_t.ni_qual_name ~f:String.equal
  with
  | List.Or_unequal_lengths.Ok b ->
      b
  | List.Or_unequal_lengths.Unequal_lengths ->
      false


let get_decl decl_ptr =
  let decl = Int.Table.find ClangPointers.pointer_decl_table decl_ptr in
  match decl with
  | Some (VarDecl ({di_parent_pointer= Some parent_pointer}, ndi, _, _)) -> (
    match Int.Table.find ClangPointers.pointer_decl_table parent_pointer with
    | Some (CXXRecordDecl (_, _, _, decls, _, _, _, _)) -> (
        let has_same_ndi = function
          | Clang_ast_t.VarDecl (_, ndi', _, _) ->
              named_decl_info_equal ndi ndi'
          | _ ->
              false
        in
        match List.find decls ~f:has_same_ndi with Some _ as decl' -> decl' | None -> decl )
    | _ ->
        decl )
  | _ ->
      decl


let get_decl_opt decl_ptr_opt =
  match decl_ptr_opt with Some decl_ptr -> get_decl decl_ptr | None -> None


let get_stmt stmt_ptr source_range =
  let stmt = Int.Table.find ClangPointers.pointer_stmt_table stmt_ptr in
  if Option.is_none stmt then
    CFrontend_config.incorrect_assumption __POS__ source_range "stmt with pointer %d not found@\n"
      stmt_ptr ;
  stmt


let get_stmt_opt stmt_ptr_opt source_range =
  match stmt_ptr_opt with Some stmt_ptr -> get_stmt stmt_ptr source_range | None -> None


let get_decl_opt_with_decl_ref decl_ref_opt =
  match decl_ref_opt with
  | Some decl_ref ->
      get_decl decl_ref.Clang_ast_t.dr_decl_pointer
  | None ->
      None


let get_property_of_ivar decl_ptr = Int.Table.find ClangPointers.ivar_to_property_table decl_ptr

let update_sil_types_map type_ptr sil_type =
  CFrontend_config.sil_types_map :=
    Clang_ast_extend.TypePointerMap.add type_ptr sil_type !CFrontend_config.sil_types_map


let update_enum_map enum_constant_pointer sil_exp =
  let predecessor_pointer_opt, _ =
    ClangPointers.Map.find_exn !CFrontend_config.enum_map enum_constant_pointer
  in
  let enum_map_value = (predecessor_pointer_opt, Some sil_exp) in
  CFrontend_config.enum_map :=
    ClangPointers.Map.set !CFrontend_config.enum_map ~key:enum_constant_pointer
      ~data:enum_map_value


let add_enum_constant enum_constant_pointer predecessor_pointer_opt =
  let enum_map_value = (predecessor_pointer_opt, None) in
  CFrontend_config.enum_map :=
    ClangPointers.Map.set !CFrontend_config.enum_map ~key:enum_constant_pointer
      ~data:enum_map_value


let get_enum_constant_exp enum_constant_pointer =
  ClangPointers.Map.find_exn !CFrontend_config.enum_map enum_constant_pointer


let get_type type_ptr =
  match type_ptr with
  (* There is chance for success only if type_ptr is in fact clang pointer *)
  | Clang_ast_types.TypePtr.Ptr raw_ptr ->
      Int.Table.find ClangPointers.pointer_type_table raw_ptr
  | _ ->
      (* TODO(T30739447): investigate why this happens *)
      (* otherwise, function fails *)
      let type_str = Clang_ast_extend.type_ptr_to_string type_ptr in
      L.(debug Capture Medium) "type %s is not clang pointer@\n" type_str ;
      None


let get_desugared_type type_ptr =
  let typ_opt = get_type type_ptr in
  match typ_opt with
  | Some typ -> (
      let type_info = Clang_ast_proj.get_type_tuple typ in
      match type_info.Clang_ast_t.ti_desugared_type with Some ptr -> get_type ptr | _ -> typ_opt )
  | _ ->
      typ_opt


let get_decl_from_typ_ptr typ_ptr =
  let typ_opt = get_desugared_type typ_ptr in
  let typ = match typ_opt with Some t -> t | None -> assert false in
  match (typ : Clang_ast_t.c_type) with
  | RecordType (_, decl_ptr) | ObjCInterfaceType (_, decl_ptr) ->
      get_decl decl_ptr
  | _ ->
      None


let sil_annot_of_type {Clang_ast_t.qt_type_ptr} =
  let default_visibility = true in
  let mk_annot annot_name_opt =
    match annot_name_opt with
    | Some annot_name ->
        [({Annot.class_name= annot_name; parameters= []}, default_visibility)]
    | None ->
        Annot.Item.empty
  in
  let annot_name_opt =
    match get_type qt_type_ptr with
    | Some (AttributedType (_, attr_info)) ->
        if attr_info.ati_attr_kind = `Nullable then Some Annotations.nullable
        else if attr_info.ati_attr_kind = `Nonnull then Some Annotations.nonnull
          (* other annotations go here *)
        else None
    | _ ->
        None
  in
  mk_annot annot_name_opt


let name_of_typedef_type_info {Clang_ast_t.tti_decl_ptr} =
  match get_decl tti_decl_ptr with
  | Some (TypedefDecl (_, name_decl_info, _, _)) ->
      get_qualified_name name_decl_info
  | _ ->
      QualifiedCppName.empty


let name_opt_of_typedef_qual_type qual_type =
  match get_type qual_type.Clang_ast_t.qt_type_ptr with
  | Some (Clang_ast_t.TypedefType (_, typedef_type_info)) ->
      Some (name_of_typedef_type_info typedef_type_info)
  | _ ->
      None


let qual_type_of_decl_ptr decl_ptr =
  { (* This function needs to be in this module - CAst_utils can't depend on
     Ast_expressions *)
    Clang_ast_t.qt_type_ptr= Clang_ast_extend.DeclPtr decl_ptr
  ; qt_is_const= false
  ; qt_is_volatile= false
  ; qt_is_restrict= false }


let add_type_from_decl_ref qual_type_to_sil_type tenv dr =
  let qual_type = qual_type_of_decl_ptr dr.Clang_ast_t.dr_decl_pointer in
  ignore (qual_type_to_sil_type tenv qual_type)


let add_type_from_decl_ref_opt qual_type_to_sil_type tenv decl_ref_opt fail_if_not_found =
  match decl_ref_opt with
  (* translate interface first if found *)
  | Some dr ->
      add_type_from_decl_ref qual_type_to_sil_type tenv dr
  | _ ->
      if fail_if_not_found then assert false else ()


let add_type_from_decl_ref_list qual_type_to_sil_type tenv decl_ref_list =
  List.iter ~f:(add_type_from_decl_ref qual_type_to_sil_type tenv) decl_ref_list


let get_function_decl_with_body decl_ptr =
  let open Clang_ast_t in
  let decl_opt = get_decl decl_ptr in
  let decl_ptr' =
    match decl_opt with
    | Some (FunctionDecl (_, _, _, fdecl_info))
    | Some (CXXMethodDecl (_, _, _, fdecl_info, _))
    | Some (CXXConstructorDecl (_, _, _, fdecl_info, _))
    | Some (CXXConversionDecl (_, _, _, fdecl_info, _))
    | Some (CXXDestructorDecl (_, _, _, fdecl_info, _)) ->
        fdecl_info.Clang_ast_t.fdi_decl_ptr_with_body
    | _ ->
        Some decl_ptr
  in
  if [%compare.equal: int option] decl_ptr' (Some decl_ptr) then decl_opt
  else get_decl_opt decl_ptr'


let get_info_from_decl_ref decl_ref =
  let name_info = match decl_ref.Clang_ast_t.dr_name with Some ni -> ni | _ -> assert false in
  let decl_ptr = decl_ref.Clang_ast_t.dr_decl_pointer in
  let qual_type =
    match decl_ref.Clang_ast_t.dr_qual_type with Some tp -> tp | _ -> assert false
  in
  (name_info, decl_ptr, qual_type)


(* st |= EF (atomic_pred param) *)
let rec exists_eventually_st atomic_pred param st =
  if atomic_pred param st then true
  else
    let _, st_list = Clang_ast_proj.get_stmt_tuple st in
    List.exists ~f:(exists_eventually_st atomic_pred param) st_list


let is_syntactically_global_var decl =
  match decl with
  | Clang_ast_t.VarDecl (_, _, _, vdi) ->
      vdi.vdi_is_global && not vdi.vdi_is_static_local
  | _ ->
      false


let is_static_local_var decl =
  match decl with Clang_ast_t.VarDecl (_, _, _, vdi) -> vdi.vdi_is_static_local | _ -> false


let is_const_expr_var decl =
  match decl with Clang_ast_t.VarDecl (_, _, _, vdi) -> vdi.vdi_is_const_expr | _ -> false


let full_name_of_decl_opt decl_opt =
  match decl_opt with
  | Some decl -> (
    match Clang_ast_proj.get_named_decl_tuple decl with
    | Some (_, name_info) ->
        get_qualified_name name_info
    | None ->
        QualifiedCppName.empty )
  | None ->
      QualifiedCppName.empty


(* Generates a unique number for each variant of a type. *)
let get_tag ast_item =
  let item_rep = Obj.repr ast_item in
  if Obj.is_block item_rep then Obj.tag item_rep else -Obj.obj item_rep


(* Generates a key for a statement based on its sub-statements and the statement tag. *)
let generate_key_stmt stmt =
  let buffer = Buffer.create 16 in
  let rec add_stmt stmt =
    Buffer.add_string buffer (string_of_int (get_tag stmt)) ;
    let _, stmts = Clang_ast_proj.get_stmt_tuple stmt in
    List.iter ~f:add_stmt stmts
  in
  add_stmt stmt ; Buffer.contents buffer


(* Generates a key for a declaration based on its name and the declaration tag. *)
let generate_key_decl decl =
  let name = full_name_of_decl_opt (Some decl) in
  Format.sprintf "%d%s" (get_tag decl) (QualifiedCppName.to_qual_string name)


let rec get_super_if decl =
  match decl with
  | Some (Clang_ast_t.ObjCImplementationDecl (_, _, _, _, impl_decl_info)) ->
      (* Try getting the super ref through the impl info, and fall back to
         getting the if decl first and getting the super ref through it. *)
      let super_ref = get_decl_opt_with_decl_ref impl_decl_info.oidi_super in
      if Option.is_some super_ref then super_ref
      else get_super_if (get_decl_opt_with_decl_ref impl_decl_info.oidi_class_interface)
  | Some (Clang_ast_t.ObjCInterfaceDecl (_, _, _, _, interface_decl_info)) ->
      get_decl_opt_with_decl_ref interface_decl_info.otdi_super
  | _ ->
      None


let get_super_ObjCImplementationDecl impl_decl_info =
  let objc_interface_decl_current =
    get_decl_opt_with_decl_ref impl_decl_info.Clang_ast_t.oidi_class_interface
  in
  let objc_interface_decl_super = get_super_if objc_interface_decl_current in
  let objc_implementation_decl_super =
    match objc_interface_decl_super with
    | Some (ObjCInterfaceDecl (_, _, _, _, interface_decl_info)) ->
        get_decl_opt_with_decl_ref interface_decl_info.otdi_implementation
    | _ ->
        None
  in
  objc_implementation_decl_super


let get_impl_decl_info dec =
  match dec with Clang_ast_t.ObjCImplementationDecl (_, _, _, _, idi) -> Some idi | _ -> None


let default_blacklist = CFrontend_config.[nsobject_cl; nsproxy_cl]

let rec is_objc_if_descendant ?(blacklist = default_blacklist) if_decl ancestors =
  (* List of ancestors to check for and list of classes to short-circuit to
     false can't intersect *)
  if not String.Set.(is_empty (inter (of_list blacklist) (of_list ancestors))) then
    L.(die InternalError) "Blacklist and ancestors must be mutually exclusive."
  else
    match if_decl with
    | Some (Clang_ast_t.ObjCInterfaceDecl (_, ndi, _, _, _)) ->
        let in_list some_list = List.mem ~equal:String.equal some_list ndi.Clang_ast_t.ni_name in
        (not (in_list blacklist))
        && (in_list ancestors || is_objc_if_descendant ~blacklist (get_super_if if_decl) ancestors)
    | _ ->
        false


let rec qual_type_to_objc_interface qual_type =
  let typ_opt = get_desugared_type qual_type.Clang_ast_t.qt_type_ptr in
  ctype_to_objc_interface typ_opt


and ctype_to_objc_interface typ_opt =
  match (typ_opt : Clang_ast_t.c_type option) with
  | Some (ObjCInterfaceType (_, decl_ptr)) ->
      get_decl decl_ptr
  | Some (ObjCObjectPointerType (_, (inner_qual_type : Clang_ast_t.qual_type))) ->
      qual_type_to_objc_interface inner_qual_type
  | Some (FunctionProtoType (_, function_type_info, _))
  | Some (FunctionNoProtoType (_, function_type_info)) ->
      qual_type_to_objc_interface function_type_info.Clang_ast_t.fti_return_type
  | _ ->
      None


let if_decl_to_di_pointer_opt if_decl =
  match if_decl with
  | Some (Clang_ast_t.ObjCInterfaceDecl (if_decl_info, _, _, _, _)) ->
      Some if_decl_info.di_pointer
  | _ ->
      None


let is_instance_type qual_type =
  match name_opt_of_typedef_qual_type qual_type with
  | Some name ->
      String.equal (QualifiedCppName.to_qual_string name) "instancetype"
  | None ->
      false


let return_type_matches_class_type result_type interface_decl =
  if is_instance_type result_type then true
  else
    let return_type_decl_opt = qual_type_to_objc_interface result_type in
    [%compare.equal: int option]
      (if_decl_to_di_pointer_opt interface_decl)
      (if_decl_to_di_pointer_opt return_type_decl_opt)


let is_objc_factory_method ~class_decl:interface_decl ~method_decl:meth_decl_opt =
  let open Clang_ast_t in
  match meth_decl_opt with
  | Some (ObjCMethodDecl (_, _, omdi)) ->
      (not omdi.omdi_is_instance_method)
      && return_type_matches_class_type omdi.omdi_result_type interface_decl
  | _ ->
      false


let name_of_decl_ref_opt (decl_ref_opt : Clang_ast_t.decl_ref option) =
  match decl_ref_opt with
  | Some decl_ref -> (
    match decl_ref.dr_name with Some named_decl_info -> Some named_decl_info.ni_name | _ -> None )
  | _ ->
      None


let type_of_decl decl =
  let open Clang_ast_t in
  match decl with
  | ObjCMethodDecl (_, _, obj_c_method_decl_info) ->
      Some obj_c_method_decl_info.omdi_result_type.qt_type_ptr
  | ObjCPropertyDecl (_, _, obj_c_property_decl_info) ->
      Some obj_c_property_decl_info.opdi_qual_type.qt_type_ptr
  | EnumDecl (_, _, type_ptr, _, _, _, _)
  | RecordDecl (_, _, type_ptr, _, _, _, _)
  | CXXRecordDecl (_, _, type_ptr, _, _, _, _, _)
  | ClassTemplateSpecializationDecl (_, _, type_ptr, _, _, _, _, _, _, _)
  | ClassTemplatePartialSpecializationDecl (_, _, type_ptr, _, _, _, _, _, _, _)
  | TemplateTypeParmDecl (_, _, type_ptr)
  | ObjCTypeParamDecl (_, _, type_ptr)
  | TypeAliasDecl (_, _, type_ptr)
  | TypedefDecl (_, _, type_ptr, _)
  | UnresolvedUsingTypenameDecl (_, _, type_ptr) ->
      Some type_ptr
  | BindingDecl (_, _, qual_type)
  | FieldDecl (_, _, qual_type, _)
  | ObjCAtDefsFieldDecl (_, _, qual_type, _)
  | ObjCIvarDecl (_, _, qual_type, _, _)
  | FunctionDecl (_, _, qual_type, _)
  | CXXMethodDecl (_, _, qual_type, _, _)
  | CXXConstructorDecl (_, _, qual_type, _, _)
  | CXXConversionDecl (_, _, qual_type, _, _)
  | CXXDestructorDecl (_, _, qual_type, _, _)
  | MSPropertyDecl (_, _, qual_type)
  | NonTypeTemplateParmDecl (_, _, qual_type)
  | VarDecl (_, _, qual_type, _)
  | DecompositionDecl (_, _, qual_type, _)
  | ImplicitParamDecl (_, _, qual_type, _)
  | OMPCapturedExprDecl (_, _, qual_type, _)
  | ParmVarDecl (_, _, qual_type, _)
  | VarTemplateSpecializationDecl (_, _, qual_type, _)
  | VarTemplatePartialSpecializationDecl (_, _, qual_type, _)
  | EnumConstantDecl (_, _, qual_type, _)
  | IndirectFieldDecl (_, _, qual_type, _)
  | OMPDeclareReductionDecl (_, _, qual_type)
  | UnresolvedUsingValueDecl (_, _, qual_type) ->
      Some qual_type.qt_type_ptr
  | _ ->
      None


let get_record_fields decl =
  let open Clang_ast_t in
  match decl with
  | ClassTemplateSpecializationDecl (_, _, _, decl_list, _, _, _, _, _, _)
  | CXXRecordDecl (_, _, _, decl_list, _, _, _, _)
  | RecordDecl (_, _, _, decl_list, _, _, _) ->
      List.filter ~f:(function FieldDecl _ -> true | _ -> false) decl_list
  | _ ->
      []


let get_cxx_base_classes decl =
  let open Clang_ast_t in
  match decl with
  | CXXRecordDecl (_, _, _, _, _, _, _, cxx_record_info)
  | ClassTemplateSpecializationDecl (_, _, _, _, _, _, _, cxx_record_info, _, _) ->
      cxx_record_info.xrdi_bases
  | _ ->
      []


let get_cxx_virtual_base_classes decl =
  let open Clang_ast_t in
  match decl with
  | CXXRecordDecl (_, _, _, _, _, _, _, cxx_record_info)
  | ClassTemplateSpecializationDecl (_, _, _, _, _, _, _, cxx_record_info, _, _) ->
      cxx_record_info.xrdi_transitive_vbases
  | _ ->
      []


let is_std_vector qt =
  match get_decl_from_typ_ptr qt.Clang_ast_t.qt_type_ptr with
  | Some decl -> (
    match Clang_ast_proj.get_named_decl_tuple decl with
    | Some (_, qual_name) ->
        String.equal qual_name.ni_name "vector"
        && List.mem ~equal:String.equal qual_name.ni_qual_name "std"
    | None ->
        false )
  | None ->
      false


let has_block_attribute decl =
  let open Clang_ast_t in
  match decl with
  | VarDecl (decl_info, _, _, _) ->
      let attributes = decl_info.di_attributes in
      List.exists ~f:(fun attr -> match attr with BlocksAttr _ -> true | _ -> false) attributes
  | _ ->
      false


let is_implicit_decl decl =
  let decl_info = Clang_ast_proj.get_decl_tuple decl in
  decl_info.Clang_ast_t.di_is_implicit


let get_superclass_curr_class_objc_from_decl (decl : Clang_ast_t.decl) =
  match decl with
  | ObjCInterfaceDecl (_, _, _, _, otdi) ->
      otdi.otdi_super
  | ObjCImplementationDecl (_, ni, _, _, oi) -> (
    match
      oi.Clang_ast_t.oidi_class_interface
      |> Option.map ~f:(fun dr -> dr.Clang_ast_t.dr_decl_pointer)
      |> Option.value_map ~f:get_decl ~default:None
    with
    | Some (ObjCInterfaceDecl (_, _, _, _, otdi)) ->
        otdi.otdi_super
    | _ ->
        Logging.die InternalError
          "Expected that ObjCImplementationDecl always has a pointer to it's interface, but \
           wasn't the case with %s"
          ni.Clang_ast_t.ni_name )
  | ObjCCategoryDecl (_, _, _, _, ocdi) ->
      ocdi.odi_class_interface
  | ObjCCategoryImplDecl (_, _, _, _, ocidi) ->
      ocidi.ocidi_class_interface
  | decl ->
      Logging.die InternalError
        "Expected to be called only with ObjCInterfaceDecl, ObjCImplementationDecl, \
         ObjCCategoryDecl or ObjCCategoryImplDecl, but got %s"
        (Clang_ast_proj.get_decl_kind_string decl)
