(*---------------------------------------------------------------------------
   Copyright (c) 2015 The bos programmers. All rights reserved.
   Distributed under the ISC license, see terms at the end of the file.
  ---------------------------------------------------------------------------*)

open Astring
open Rresult

let uerror = Unix.error_message

(* Existence *)

let rec file_exists file =
  try Ok (Unix.((stat @@ Fpath.to_string file).st_kind = S_REG)) with
  | Unix.Unix_error (Unix.ENOENT, _, _) -> Ok false
  | Unix.Unix_error (Unix.EINTR, _, _) -> file_exists file
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "file %a exists: %s" Fpath.pp file (uerror e)

let rec dir_exists dir =
  try Ok (Unix.((stat @@ Fpath.to_string dir).st_kind = S_DIR)) with
  | Unix.Unix_error (Unix.ENOENT, _, _) -> Ok false
  | Unix.Unix_error (Unix.EINTR, _, _) -> dir_exists dir
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "directory %a exists: %s" Fpath.pp dir (uerror e)

let rec exists path =
  try Ok (ignore @@ Unix.stat (Fpath.to_string path); true) with
  | Unix.Unix_error ((Unix.ENOENT | Unix.ENOTDIR), _, _) -> Ok false
  | Unix.Unix_error (Unix.EINTR, _, _) -> exists path
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "path %a exists: %s" Fpath.pp path (uerror e)

let rec file_must_exist file =
  try match Unix.((stat @@ Fpath.to_string file).st_kind) with
  | Unix.S_REG -> Ok file
  | _ -> R.error_msgf "%a: Not a file" Fpath.pp file
  with
  | Unix.Unix_error (Unix.ENOENT, _, _) ->
      R.error_msgf "%a: No such file" Fpath.pp file
  | Unix.Unix_error (Unix.EINTR, _, _) -> file_must_exist file
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "file %a must exist: %s" Fpath.pp file (uerror e)

let rec dir_must_exist dir =
  try match Unix.((stat @@ Fpath.to_string dir).st_kind) with
  | Unix.S_DIR -> Ok dir
  | _ -> R.error_msgf "%a: Not a directory" Fpath.pp dir
  with
  | Unix.Unix_error (Unix.ENOENT, _, _) ->
      R.error_msgf "%a: No such directory" Fpath.pp dir
  | Unix.Unix_error (Unix.EINTR, _, _) -> dir_must_exist dir
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "directory  %a must exist: %s" Fpath.pp dir (uerror e)

let rec must_exist path =
  try ignore @@ Unix.stat (Fpath.to_string path); Ok path with
  | Unix.Unix_error (Unix.ENOENT, _, _) ->
      R.error_msgf "%a: No such path" Fpath.pp path
  | Unix.Unix_error (Unix.EINTR, _, _) -> must_exist path
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "path %a must exist: %s" Fpath.pp path (uerror e)

(* Delete *)

let delete_file ?(must_exist = false) file =
  let rec unlink file = try Ok (Unix.unlink @@ Fpath.to_string file) with
  | Unix.Unix_error (Unix.ENOENT, _, _) ->
      if not must_exist then Ok () else
      R.error_msgf "delete file %a: No such file" Fpath.pp file
  | Unix.Unix_error (Unix.EINTR, _, _) -> unlink file
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "delete file %a: %s" Fpath.pp file (uerror e)
  in
  unlink file

let delete_dir ?must_exist:(must = false) ?(recurse = false) dir =
  let rec delete_files to_rmdir dirs = match dirs with
  | [] -> Ok to_rmdir
  | dir :: todo ->
      let rec delete_dir_files dh dirs =
        match (try Some (Unix.readdir dh) with End_of_file -> None) with
        | None -> Ok dirs
        | Some (".." | ".") -> delete_dir_files dh dirs
        | Some file ->
            let rec try_unlink file =
              try (Unix.unlink (Fpath.to_string file); Ok dirs) with
              | Unix.Unix_error (Unix.ENOENT, _, _) -> Ok dirs
              | Unix.Unix_error ((Unix.EISDIR (* Linux *)
                                 |Unix.EPERM), _, _) -> Ok (file :: dirs)
              | Unix.Unix_error ((Unix.EACCES, _, _)) when Sys.win32 ->
                  (* That's what Unix uses on Windows
                     https://msdn.microsoft.com/en-us/library/1c3tczd6.aspx
                     and it's rather unhelpful w.r.t. error codes. *)
                  Ok (file :: dirs)
              | Unix.Unix_error (Unix.EINTR, _, _) -> try_unlink file
              | Unix.Unix_error (e, _, _) ->
                  R.error_msgf "%a: %s" Fpath.pp file (uerror e)
            in
            match try_unlink Fpath.(dir / file) with
            | Ok dirs -> delete_dir_files dh dirs
            | Error _ as e -> e
      in
      try
        let dh = Unix.opendir (Fpath.to_string dir) in
        match Bos_base.apply (delete_dir_files dh) [] ~finally:Unix.closedir dh
        with
        | Ok dirs -> delete_files (dir :: to_rmdir) (List.rev_append dirs todo)
        | Error _ as e -> e
      with
      | Unix.Unix_error (Unix.ENOENT, _, _) -> delete_files to_rmdir todo
      | Unix.Unix_error (Unix.EINTR, _, _) -> delete_files to_rmdir dirs
      | Unix.Unix_error (e, _, _) ->
          R.error_msgf "%a: %s" Fpath.pp dir (uerror e)
  in
  let rec delete_dirs = function
  | [] -> Ok ()
  | dir :: dirs ->
      let rec rmdir dir = try Ok (Unix.rmdir (Fpath.to_string dir)) with
      | Unix.Unix_error (Unix.ENOENT, _, _) -> Ok ()
      | Unix.Unix_error (Unix.EINTR, _, _) -> rmdir dir
      | Unix.Unix_error (e, _, _) ->
          R.error_msgf "%a: %s" Fpath.pp dir (uerror e)
      in
      match rmdir dir with
      | Ok () -> delete_dirs dirs
      | Error _ as e -> e
  in
  let delete recurse dir =
    if not recurse then
      let rec rmdir dir = try Ok (Unix.rmdir (Fpath.to_string dir)) with
      | Unix.Unix_error (Unix.ENOENT, _, _) -> Ok ()
      | Unix.Unix_error (Unix.EINTR, _, _) -> rmdir dir
      | Unix.Unix_error (e, _, _) -> R.error_msgf "%s" (uerror e)
      in
      rmdir dir
    else
    delete_files [] [dir] >>= fun rmdirs ->
    delete_dirs rmdirs
  in
  begin
    (if must then dir_must_exist dir else Ok dir)
    >>= fun dir -> delete recurse dir
  end
  |> R.reword_error_msg ~replace:true
    (fun msg -> R.msgf "delete directory %a: %s" Fpath.pp dir msg)

let rec delete ?(must_exist = false) ?(recurse = false) path =
  try match Unix.((stat (Fpath.to_string path)).st_kind) with
  | Unix.S_DIR -> delete_dir ~must_exist ~recurse path
  | _ -> delete_file ~must_exist path
  with
  | Unix.Unix_error (Unix.ENOENT, _, _) ->
      if not must_exist then Ok () else
      R.error_msgf "delete path %a: No such path" Fpath.pp path
  | Unix.Unix_error (Unix.EINTR, _, _) -> delete ~must_exist ~recurse path
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "delete path %a: %s" Fpath.pp path (uerror e)

(* Move, stat and mode *)

let move ?(force = false) src dst =
  let rename src dst =
    try Ok (Unix.rename (Fpath.to_string src) (Fpath.to_string dst)) with
    | Unix.Unix_error (e, _, _) ->
        R.error_msgf "move %a to %a: %s"
          Fpath.pp src Fpath.pp dst (uerror e)
  in
  if force then rename src dst else
  exists dst >>= function
  | false -> rename src dst
  | true ->
      R.error_msgf "move %a to %a: Destination exists"
        Fpath.pp src Fpath.pp dst

let rec stat p = try Ok (Unix.stat (Fpath.to_string p)) with
| Unix.Unix_error (Unix.EINTR, _, _) -> stat p
| Unix.Unix_error (e, _, _) ->
    R.error_msgf "stat %a: %s" Fpath.pp p (uerror e)

module Mode = struct
  type t = int

  let rec get p = try Ok (Unix.((stat (Fpath.to_string p)).st_perm)) with
  | Unix.Unix_error (Unix.EINTR, _, _) -> get p
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "get mode %a: %s" Fpath.pp p (uerror e)

  let rec set p m = try Ok (Unix.chmod (Fpath.to_string p) m) with
  | Unix.Unix_error (Unix.EINTR, _, _) -> set p m
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "set mode %a: %s" Fpath.pp p (uerror e)
end

(* Path links *)

let rec force_remove op target p =
  let sp = Fpath.to_string p in
  try match Unix.((lstat sp).st_kind) with
  | Unix.S_DIR -> Ok (Unix.rmdir sp)
  | _ -> Ok (Unix.unlink sp)
  with
  | Unix.Unix_error (Unix.EINTR, _, _) -> force_remove op target p
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "force %s %a to %a: %s" op Fpath.pp target Fpath.pp p
        (uerror e)

let rec link ?(force = false) ~target p =
  try Ok (Unix.link (Fpath.to_string target) (Fpath.to_string p)) with
  | Unix.Unix_error (Unix.EEXIST, _, _) when force ->
      force_remove "link" target p >>= fun () -> link ~force ~target p
  | Unix.Unix_error (Unix.EINTR, _, _) -> link ~force ~target p
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "link %a to %a: %s"
        Fpath.pp target Fpath.pp p (uerror e)

let rec symlink ?(force = false) ~target p =
  try Ok (Unix.symlink (Fpath.to_string target) (Fpath.to_string p)) with
  | Unix.Unix_error (Unix.EEXIST, _, _) when force ->
      force_remove "symlink" target p >>= fun () -> symlink ~force ~target p
  | Unix.Unix_error (Unix.EINTR, _, _) -> symlink ~force ~target p
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "symlink %a to %a: %s"
        Fpath.pp target Fpath.pp p (uerror e)

let rec symlink_target p =
  try
    let l = Unix.readlink (Fpath.to_string p) in
    match Fpath.of_string l with
    | Ok l -> Ok l
    | Error _ ->
        R.error_msgf "target of %a: could not read a path from %a"
          Fpath.pp p String.dump l
  with
  | Unix.Unix_error (Unix.EINVAL, _, _) ->
      R.error_msgf "target of %a: Not a symbolic link" Fpath.pp p
  | Unix.Unix_error (Unix.EINTR, _, _) -> symlink_target p
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "target of %a: %s" Fpath.pp p (uerror e)

let rec symlink_stat p = try Ok (Unix.lstat (Fpath.to_string p)) with
| Unix.Unix_error (Unix.EINTR, _, _) -> symlink_stat p
| Unix.Unix_error (e, _, _) ->
    R.error_msgf "symlink stat %a: %s" Fpath.pp p (uerror e)

(* Matching paths. *)

(* The following code is horribly messy mainly due to volume
   handling. Could certainly be improved. *)

let rec match_segment dotfiles ~env acc path seg =
  (* N.B. path can be empty, usually for relative patterns without volume. *)
  let var_start = match seg with Bos_pat.Var _ :: _ -> true | _ -> false in
  let rec readdir dh acc =
    match (try Some (Unix.readdir dh) with End_of_file -> None) with
    | None -> Ok acc
    | Some (".." | ".") -> readdir dh acc
    | Some e when String.length e > 1 && e.[0] = '.' && not dotfiles &&
                  var_start ->
        readdir dh acc
    | Some e ->
        match Fpath.is_seg e with
        | true ->
            begin match Bos_pat.match_pat ~env 0 e seg with
            | None -> readdir dh acc
            | Some _ as m ->
                let p =
                  if path = "" then e else
                  Fpath.(to_string (add_seg (Fpath.v path) e))
                in
                readdir dh ((p, m) :: acc)
            end
        | false ->
            R.error_msgf
              "directory %a: cannot parse element to a path (%a)"
              Fpath.pp (Fpath.v path) String.dump e
  in
  try
    let path = if path = "" then "." else path in
    let dh = Unix.opendir path in
    Bos_base.apply (readdir dh) acc ~finally:Unix.closedir dh
  with
  | Unix.Unix_error (Unix.ENOTDIR, _, _) -> Ok acc
  | Unix.Unix_error (Unix.ENOENT, _, _) -> Ok acc
  | Unix.Unix_error (Unix.EINTR, _, _) ->
      match_segment dotfiles ~env acc path seg
  | Unix.Unix_error (e, _, _) ->
      R.error_msgf "directory %a: %s" Fpath.pp (Fpath.v path) (uerror e)

let match_path ?(dotfiles = false) ~env p =
  let err _ =
    R.msgf "Unexpected error while matching `%a'" Fpath.pp p
  in
  let vol, start, segs =
    let vol, segs = Fpath.split_volume p in
    match Fpath.segs segs with
    | "" :: "" :: [] (* root *) ->  vol, Fpath.dir_sep, []
    | "" :: ss -> vol, Fpath.dir_sep, ss
    | ss -> vol, "", ss (* N.B. ss is non empty. *)
  in
  let rec match_segs acc = function
  | [] -> Ok acc
  | "" :: [] -> (* final empty segment "", keep only directories. *)
      let rec loop acc = function
      | [] -> Ok acc
      | (p, env) :: matches ->
          let r = try Ok (Unix.((stat p).st_kind = Unix.S_DIR)) with
          | Unix.Unix_error (e, _, _) -> R.error_msgf "%s: %s" p (uerror e)
          in
          match r with
          | Error _ as e -> e
          | Ok false -> loop acc matches
          | Ok true ->
              let acc' = Fpath.(to_string (add_seg (v p) ""), env) :: acc in
              loop acc' matches
      in
      loop [] acc
  | (".." | "." as e) :: segs ->
      (* We simply add the segment to current matches. No need
         to test if the resulting path exists. We can always go up (root
         absorbs) or stay at the same level. *)
      let rec loop acc = function
      | [] -> acc
      | (p, env) :: matches ->
          let p =
            if p = vol then p ^ e (* C:.. *) else
            Fpath.(to_string (add_seg (v p) e))
          in
          loop ((p, env) :: acc) matches
      in
      match_segs (loop [] acc) segs
  | seg :: segs ->
      match Bos_pat.of_string seg with
      | Error _ as e -> e
      | Ok seg ->
          let rec loop acc = function
          | [] -> Ok acc
          | (p, env) :: matches ->
              match match_segment dotfiles ~env acc p seg with
              | Error _ as e -> e
              | Ok acc -> loop acc matches
          in
          match loop [] acc with
          | Error _ as e -> e
          | Ok acc -> match_segs acc segs
  in
  let start_exists vol start =
    let start = if start = "" then "." else start in
    exists (Fpath.v (vol ^ start))
  in
  start_exists vol start >>= function
  | false -> Ok []
  | true ->
      let start = if start = "" then vol else vol ^ start in
      R.reword_error_msg err @@ match_segs [start, env] segs

let matches ?dotfiles p =
  let get_path acc (p, _) = (Fpath.v p) :: acc in
  match_path ?dotfiles ~env:None p >>| List.fold_left get_path []

let query ?dotfiles ?(init = String.Map.empty) p =
  let env = Some init in
  let unopt_map acc (p, map) = match map with
  | None -> assert false
  | Some map -> (Fpath.v p, map) :: acc
  in
  match_path ?dotfiles ~env p >>| List.fold_left unopt_map []

(* Folding over file system hierarchies *)

type 'a res = ('a, R.msg) result
type traverse = [ `Any | `None | `Sat of Fpath.t -> bool res ]
type elements = [ `Any | `Files | `Dirs | `Sat of Fpath.t -> bool res ]
type 'a fold_error = Fpath.t -> 'a res -> unit res

let log_fold_error ~level =
  fun p -> function
  | Error (`Msg e) -> Bos_log.msg level (fun m -> m "%s" e); Ok ()
  | Ok _ -> assert false

exception Fold_stop of R.msg

let err_fun err f ~backup_value = (* handles path function errors in folds *)
  fun p -> match f p with
  | Ok v -> v
  | Error _ as e ->
      match err p e with
      | Ok () -> backup_value   (* use backup value and continue the fold. *)
      | Error m -> raise (Fold_stop m)                  (* the fold stops. *)

let err_predicate_fun err p = err_fun err p ~backup_value:false

let do_traverse_fun err = function
| `Any -> fun _ -> true
| `None -> fun _ -> false
| `Sat sat -> err_predicate_fun err sat

let is_element_fun err = function
| `Any -> err_predicate_fun err exists
| `Files -> err_predicate_fun err file_exists
| `Dirs -> err_predicate_fun err dir_exists
| `Sat sat -> err_predicate_fun err sat

let is_dir_fun err =
  let is_dir p = try Ok (Sys.is_directory (Fpath.to_string p)) with
  | Sys_error e -> R.error_msg e
  in
  err_predicate_fun err is_dir

let readdir_fun err =
  let readdir d = try Ok (Sys.readdir (Fpath.to_string d)) with
  | Sys_error e -> R.error_msg e
  in
  err_fun err readdir ~backup_value:[||]

let fold
    ?(err = log_fold_error ~level:Logs.Error)
    ?(dotfiles = false)
    ?(elements = `Any) ?(traverse = `Any)
    f acc paths
  =
  try
    let do_traverse = do_traverse_fun err traverse in
    let is_element = is_element_fun err elements in
    let is_dir = is_dir_fun err in
    let readdir =  readdir_fun err in
    let process_path p (acc, to_traverse) =
      (if is_element p then (f p acc) else acc),
      (if is_dir p && do_traverse p then p :: to_traverse else to_traverse)
    in
    let dir_child d acc bname =
      if not dotfiles && String.is_prefix "." bname then acc else
      process_path Fpath.(d / bname) acc
    in
    let rec loop acc = function
    | (d :: ds) :: up ->
        let childs = readdir d in
        let acc, to_traverse = Array.fold_left (dir_child d) (acc, []) childs in
        loop acc (to_traverse :: ds :: up)
    | [] :: [] -> acc
    | [] :: up -> loop acc up
    | _ -> assert false
    in
    let init acc p =
      let base = Fpath.(basename @@ normalize p) in
      if not dotfiles && String.is_prefix "." base then acc else
      process_path p acc
    in
    let acc, to_traverse = List.fold_left init (acc, []) paths in
    (Ok (loop acc (to_traverse :: [])))
  with Fold_stop (`Msg _ as e) -> Error e

(*---------------------------------------------------------------------------
   Copyright (c) 2015 The bos programmers

   Permission to use, copy, modify, and/or distribute this software for any
   purpose with or without fee is hereby granted, provided that the above
   copyright notice and this permission notice appear in all copies.

   THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
   WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
   MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
   ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  ---------------------------------------------------------------------------*)
