open! Dune_exe
open Import
module Alias = Dune_engine.Alias
module Alias0 = Dune_rules.Alias
module Alias_builder = Dune_rules.Alias_builder

type t =
  { name : Alias.Name.t
  ; recursive : bool
  ; dir : Path.Source.t
  ; contexts : Dune_rules.Context.t list
  }

let pp { name; recursive; dir; contexts = _ } =
  let open Pp.O in
  let s =
    (if recursive then "@" else "@@")
    ^ Path.Source.to_string (Path.Source.relative dir (Alias.Name.to_string name))
  in
  let pp = Pp.verbatim "alias" ++ Pp.space ++ Pp.verbatim s in
  if recursive then Pp.verbatim "recursive" ++ Pp.space ++ pp else pp
;;

let in_dir ~name ~recursive ~contexts dir =
  let checked = Util.check_path contexts dir in
  match checked with
  | External _ ->
    User_error.raise
      [ Pp.textf "@@ on the command line must be followed by a relative path" ]
  | In_source_dir dir -> { dir; recursive; name; contexts }
  | In_private_context _ ->
    User_error.raise [ Pp.textf "no aliases in the testing context" ]
  | In_install_dir _ ->
    User_error.raise
      [ Pp.textf
          "Invalid alias: %s."
          (Path.to_string_maybe_quoted
             (Path.build Install.Context.install_context.build_dir))
      ; Pp.textf "There are no aliases in %s." (Path.to_string_maybe_quoted dir)
      ]
  | In_build_dir (ctx, dir) ->
    { dir
    ; recursive
    ; name
    ; contexts =
        [ List.find_exn contexts ~f:(fun c ->
            Context_name.equal (Context.name c) (Context.name ctx))
        ]
    }
;;

let of_string (root : Workspace_root.t) ~recursive s ~contexts =
  let path = Path.relative Path.root (root.reach_from_root_prefix ^ s) in
  if Path.is_root path
  then
    User_error.raise
      [ Pp.textf "@ on the command line must be followed by a valid alias name" ]
  else (
    let dir = Path.parent_exn path in
    let name = Alias.Name.of_string (Path.basename path) in
    in_dir ~name ~recursive ~contexts dir)
;;

let find_dir_specified_on_command_line ~dir =
  let open Memo.O in
  Source_tree.find_dir dir
  >>| function
  | Some dir -> dir
  | None ->
    User_error.raise
      [ Pp.textf
          "Don't know about directory %s specified on the command line!"
          (Path.Source.to_string_maybe_quoted dir)
      ]
;;

let dep_on_alias_multi_contexts ~dir ~name ~contexts =
  ignore (find_dir_specified_on_command_line ~dir : _ Memo.t);
  let context_to_alias_expansion ctx =
    let ctx_dir = Context_name.build_dir ctx in
    let dir = Path.Build.(append_source ctx_dir dir) in
    Alias_builder.alias (Alias.make ~dir name)
  in
  Action_builder.all_unit (List.map contexts ~f:context_to_alias_expansion)
;;

let dep_on_alias_rec_multi_contexts ~dir:src_dir ~name ~contexts =
  let open Action_builder.O in
  let* dir = Action_builder.of_memo (find_dir_specified_on_command_line ~dir:src_dir) in
  let* alias_statuses =
    Action_builder.all
      (List.map contexts ~f:(fun ctx ->
         let dir =
           Path.Build.append_source
             (Context_name.build_dir ctx)
             (Source_tree.Dir.path dir)
         in
         Dune_rules.Alias_rec.dep_on_alias_rec name dir))
  in
  match
    Alias0.is_standard name
    || List.exists alias_statuses ~f:(fun (x : Alias_builder.Alias_status.t) ->
      match x with
      | Defined -> true
      | Not_defined -> false)
  with
  | true -> Action_builder.return ()
  | false ->
    let* load_dir =
      Action_builder.all
      @@ List.map contexts ~f:(fun ctx ->
        let dir =
          Source_tree.Dir.path dir
          |> Path.Build.append_source (Context_name.build_dir ctx)
          |> Path.build
        in
        Action_builder.of_memo @@ Load_rules.load_dir ~dir)
    in
    let hints =
      let candidates =
        Alias.Name.Set.union_map load_dir ~f:(function
          | Load_rules.Loaded.Build build -> Alias.Name.Set.of_keys build.aliases
          | _ -> Alias.Name.Set.empty)
      in
      User_message.did_you_mean
        (Alias.Name.to_string name)
        ~candidates:(Alias.Name.Set.to_list_map ~f:Alias.Name.to_string candidates)
    in
    User_error.raise
      ~hints
      [ Pp.textf
          "Alias %S specified on the command line is empty."
          (Alias.Name.to_string name)
      ; Pp.textf
          "It is not defined in %s or any of its descendants."
          (Path.Source.to_string_maybe_quoted src_dir)
      ]
;;

let request { name; recursive; dir; contexts } =
  let contexts = List.map ~f:Context.name contexts in
  (if recursive then dep_on_alias_rec_multi_contexts else dep_on_alias_multi_contexts)
    ~dir
    ~name
    ~contexts
;;
