(***********************************************************************)
(*                             ocamlbuild                              *)
(*                                                                     *)
(*  Nicolas Pouillard, Berke Durak, projet Gallium, INRIA Rocquencourt *)
(*                                                                     *)
(*  Copyright 2007 Institut National de Recherche en Informatique et   *)
(*  en Automatique.  All rights reserved.  This file is distributed    *)
(*  under the terms of the Q Public License version 1.0.               *)
(*                                                                     *)
(***********************************************************************)


(* Original author: Berke Durak *)
(* Slurp *)
open My_std
open Outcome

type 'a entry =
  | Dir of string * string * My_unix.stats Lazy.t * 'a * 'a entry list Lazy.t
  | File of string * string * My_unix.stats Lazy.t * 'a
  | Error of exn
  | Nothing

let (/) = filename_concat

let rec filter predicate = function
  | Dir(path, name, st, attr, entries) ->
      if predicate path name attr then
        Dir(path, name, st, attr, lazy (List.map (filter predicate) !*entries))
      else
        Nothing
  | File(path, name, _, attr) as f ->
      if predicate path name attr then
        f
      else
        Nothing
  | Nothing -> Nothing
  | Error _ as e -> e

let real_slurp path =
  let cwd = Sys.getcwd () in
  let abs x = if Filename.is_implicit x || Filename.is_relative x then cwd/x else x in
  let visited = Hashtbl.create 1024 in
  let rec scandir path names =
    let (file_acc, dir_acc) = 
      Array.fold_left begin fun ((file_acc, dir_acc) as acc) name ->
        match do_entry true path name with
        | None -> acc
        | Some((Dir _|Error _) as entry) -> (file_acc, entry :: dir_acc)
        | Some((File _) as entry) -> (entry :: file_acc, dir_acc)
        | Some Nothing -> acc
      end
      ([], [])
      names
    in
    file_acc @ dir_acc
  and do_entry link_mode path name =
    let fn = path/name in
    let absfn = abs fn in
    match
      try
        Good(if link_mode then My_unix.lstat absfn else My_unix.stat absfn)
      with
      | x -> Bad x
    with
    | Bad x -> Some(Error x)
    | Good st ->
      let key = st.My_unix.stat_key in
      if try Hashtbl.find visited key with Not_found -> false
      then None
      else
        begin
          Hashtbl.add visited key true;
          let res =
            match st.My_unix.stat_file_kind with
            | My_unix.FK_link ->
                let fn' = My_unix.readlink absfn in
                if sys_file_exists (abs fn') then
                  do_entry false path name
                else
                  Some(File(path, name, lazy st, ()))
            | My_unix.FK_dir ->
                (match sys_readdir absfn with
                | Good names -> Some(Dir(path, name, lazy st, (), lazy (scandir fn names)))
                | Bad exn -> Some(Error exn))
            | My_unix.FK_other -> None
            | My_unix.FK_file -> Some(File(path, name, lazy st, ())) in
          Hashtbl.replace visited key false;
          res
        end
  in
  match do_entry true "" path with
  | None -> raise Not_found
  | Some entry -> entry

let split path =
  let rec aux path =
    if path = Filename.current_dir_name then []
    else (Filename.basename path) :: aux (Filename.dirname path)
  in List.rev (aux path)

let rec join =
  function
  | [] -> assert false
  | [x] -> x
  | x :: xs -> x/(join xs)

let rec add root path entries =
  match path, entries with
  | [], _ -> entries
  | xpath :: xspath, (Dir(dpath, dname, dst, dattr, dentries) as d) :: entries ->
      if xpath = dname then
        Dir(dpath, dname, dst, dattr, lazy (add (root/xpath) xspath !*dentries)) :: entries
      else d :: add root path entries
  | [xpath], [] ->
      [File(root, xpath, lazy (My_unix.stat (root/xpath)), ())]
  | xpath :: xspath, [] ->
      [Dir(root/(join xspath), xpath,
           lazy (My_unix.stat (root/(join path))), (),
           lazy (add (root/xpath) xspath []))]
  | _, Nothing :: entries -> add root path entries
  | _, Error _ :: _ -> entries
  | [xpath], (File(_, fname, _, _) as f) :: entries' ->
      if xpath = fname then entries
      else f :: add root path entries'
  | xpath :: xspath, (File(fpath, fname, fst, fattr) as f) :: entries' ->
      if xpath = fname then
        Dir(fpath, fname, fst, fattr, lazy (add (root/xpath) xspath [])) :: entries'
      else f :: add root path entries'

let slurp_with_find path =
  let lines =
    My_unix.run_and_open (Printf.sprintf "find %s" (Filename.quote path)) begin fun ic ->
      let acc = ref [] in
      try while true do acc := input_line ic :: !acc done; []
      with End_of_file -> !acc
    end in
  let res =
    List.fold_right begin fun line acc ->
      add path (split line) acc
    end lines [] in
  match res with
  | [] -> Nothing
  | [entry] -> entry
  | entries -> Dir(path, Filename.basename path, lazy (My_unix.stat path), (), lazy entries)

let slurp x = if !*My_unix.is_degraded then slurp_with_find x else real_slurp x

let rec print print_attr f entry =
  match entry with
  | Dir(path, name, _, attr, entries) ->
      Format.fprintf f "@[<2>Dir(%S,@ %S,@ _,@ %a,@ %a)@]"
        path name print_attr attr (List.print (print print_attr)) !*entries
  | File(path, name, _, attr) ->
      Format.fprintf f "@[<2>File(%S,@ %S,@ _,@ %a)@]" path name print_attr attr
  | Nothing ->
      Format.fprintf f "Nothing"
  | Error(_) ->
      Format.fprintf f "Error(_)"

let rec fold f entry acc =
  match entry with
  | Dir(path, name, _, attr, contents) ->
      f path name attr (List.fold_right (fold f) !*contents acc)
  | File(path, name, _, attr) ->
      f path name attr acc
  | Nothing | Error _ -> acc

let map f entry =
  let rec self entry =
    match entry with
    | Dir(path, name, st, attr, contents) ->
        Dir(path, name, st, f path name attr, lazy (List.map self !*contents))
    | File(path, name, st, attr) ->
        File(path, name, st, f path name attr)
    | Nothing -> Nothing
    | Error e -> Error e
  in self entry

let rec force =
  function
  | Dir(_, _, st, _, contents) ->
      let _ = !*st in List.iter force !*contents
  | File(_, _, st, _) ->
      ignore !*st
  | Nothing | Error _ -> ()
