open Stdune

module Kind = struct
  type t =
    | Explicit
    | Dune_workspace
    | Dune_project
    | Cwd

  let priority = function
    | Explicit -> 0
    | Dune_workspace -> 1
    | Dune_project -> 2
    | Cwd -> 3
  ;;

  let lowest_priority = max_int

  let of_dir_contents files =
    if String.Set.mem files Source.Workspace.filename
    then Some Dune_workspace
    else if Filename.Set.mem files Dune_lang.Dune_project.filename
    then Some Dune_project
    else None
  ;;
end

type t =
  { dir : string
  ; to_cwd : string list
  ; reach_from_root_prefix : string
  ; kind : Kind.t
  }

module Candidate = struct
  type t =
    { dir : string
    ; to_cwd : string list
    ; kind : Kind.t
    }
end

let find from =
  let cwd =
    if Filename.is_relative from then Filename.concat Fpath.initial_cwd from else from
  in
  let rec loop counter ~(candidate : Candidate.t option) ~to_cwd dir : Candidate.t option =
    match Sys.readdir dir with
    | exception Sys_error msg ->
      User_warning.emit
        [ Pp.textf
            "Unable to read directory %s. Will not look for root in parent directories."
            dir
        ; User_error.reason (Pp.verbatim msg)
        ; Pp.text "To remove this warning, set your root explicitly using --root."
        ];
      candidate
    | files ->
      let files = String.Set.of_list (Array.to_list files) in
      let candidate =
        let candidate_priority =
          match candidate with
          | Some c -> Kind.priority c.kind
          | None -> Kind.lowest_priority
        in
        match Kind.of_dir_contents files with
        | Some kind when Kind.priority kind <= candidate_priority ->
          Some { Candidate.kind; dir; to_cwd }
        | _ -> candidate
      in
      cont counter ~candidate dir ~to_cwd
  and cont counter ~candidate ~to_cwd dir =
    if counter > String.length cwd
    then candidate
    else (
      let parent = Filename.dirname dir in
      if parent = dir
      then candidate
      else (
        let base = Filename.basename dir in
        loop (counter + 1) parent ~candidate ~to_cwd:(base :: to_cwd)))
  in
  loop 0 ~to_cwd:[] cwd ~candidate:None
;;

let create ~from ~default_is_cwd ~specified_by_user () =
  match
    match specified_by_user with
    | Some dn -> Some { Candidate.kind = Explicit; dir = dn; to_cwd = [] }
    | None ->
      let cwd = { Candidate.kind = Cwd; dir = from; to_cwd = [] } in
      if Execution_env.inside_dune
      then Some cwd
      else (
        match find from with
        | Some s -> Some s
        | None -> if default_is_cwd then Some cwd else None)
  with
  | None -> None
  | Some { Candidate.dir; to_cwd; kind } ->
    Some
      { kind
      ; dir
      ; to_cwd
      ; reach_from_root_prefix =
          String.concat ~sep:"" (List.map to_cwd ~f:(sprintf "%s/"))
      }
;;

let create_exn ~from ~default_is_cwd ~specified_by_user () =
  match create ~from ~default_is_cwd ~specified_by_user () with
  | Some x -> x
  | None ->
    raise
      (User_error.E
         User_error.(
           make
             [ Pp.text "I cannot find the root of the current workspace/project."
             ; Pp.text "If you would like to create a new dune project, you can type:"
             ; Pp.nop
             ; Pp.verbatim "    dune init project NAME"
             ; Pp.nop
             ; Pp.text
                 "Otherwise, please make sure to run dune inside an existing project or \
                  workspace. For more information about how dune identifies the root of \
                  the current workspace/project, please refer to \
                  https://dune.readthedocs.io/en/stable/usage.html#finding-the-root"
             ]))
;;
