(**************************************************************************)
(*                                                                        *)
(*                                 OCaml                                  *)
(*                                                                        *)
(*             Sebastien Hinderer, projet Gallium, INRIA Paris            *)
(*                                                                        *)
(*   Copyright 2016 Institut National de Recherche en Informatique et     *)
(*     en Automatique.                                                    *)
(*                                                                        *)
(*   All rights reserved.  This file is distributed under the terms of    *)
(*   the GNU Lesser General Public License version 2.1, with the          *)
(*   special exception on linking described in the file LICENSE.          *)
(*                                                                        *)
(**************************************************************************)

(* A few extensions to OCaml's standard library *)

module Unix = Ocamltest_unix

let input_line_opt ic =
  try Some (input_line ic) with End_of_file -> None

module Char = struct
  include Char
  let is_blank c =
    c = ' ' || c = '\012' || c = '\n' || c = '\r' || c =  '\t'
end

module Filename = struct
  include Filename
  let path_sep = if Sys.win32 then ";" else ":"
  (* This function comes from otherlibs/unix/unix_win32.ml *)
  let maybe_quote f =
    if f = ""
    || String.exists (function ' ' | '\"' | '\t' -> true | _ -> false) f
    then Filename.quote f
    else f

  let make_filename name ext = String.concat "." [name; ext]

  let make_path components = List.fold_left Filename.concat "" components

  let mkexe filename = filename ^ Ocamltest_config.exe
end

module List = struct
  include List

  let rec fold_left_result f acc = function
    | [] -> Ok acc
    | x :: xs ->
        match f acc x with
        | Error _ as err -> err
        | Ok acc -> fold_left_result f acc xs
end

module String = struct
  include Misc.Stdlib.String
  let string_of_char = String.make 1

  let words s =
    let l = String.length s in
    let rec f quote w ws i =
      if i>=l then begin
        if w<>"" then List.rev (w::ws)
        else List.rev ws
      end else begin
        let j = i+1 in
        match s.[i] with
          | '\''
          | '"' as c ->
            begin
              match quote with
              | None ->
                (* Begin quoted word *)
                f (Some c) w ws j
              | Some quote_char when quote_char = c ->
                (* End quoted word *)
                f None w ws j
              | _ ->
                (* Continue string *)
                f quote (w ^ (string_of_char c)) ws j
            end
          | ' ' ->
            begin
              if quote <> None
              then f quote (w ^ (string_of_char ' ')) ws j
              else begin
                if w=""
                then f None w ws j
                else f None "" (w::ws) j
              end
            end
          | _ as c -> f quote (w ^ (string_of_char c)) ws j
      end in
    if l=0 then [] else f None "" [] 0
end

module Sys = struct
  include Sys

  let erase_file path =
    try Sys.remove path
    with Sys_error _ when Sys.win32 && Ocamltest_config.libunix <> None ->
      (* Deal with read-only attribute on Windows. Ignore any error from chmod
         so that the message always come from Sys.remove *)
      let () = try Unix.chmod path 0o666 with Sys_error _ -> () in
      Sys.remove path

  let rm_rf path =
    let rec erase path =
      (* Sys.file_exists will return false for dangling symlinks *)
      if Sys.file_exists path then
        if Sys.is_directory path then begin
          (* path might be a symlink to a directory *)
          try Sys.remove path
          with Sys_error _ ->
            (* path is definitely a directory, not a symlink to a directory *)
            Array.iter (fun entry -> erase (Filename.concat path entry))
                       (Sys.readdir path);
            Sys.rmdir path
        end else erase_file path
      else erase_file path
    in
      if Sys.file_exists path then
        try erase path
        with Sys_error err ->
          raise (Sys_error (Printf.sprintf "Failed to remove %S (%s)" path err))
      else
        (* path could be a dangling symlink *)
        try Sys.remove path
        with Sys_error _ -> ()

  let rec make_directory dir =
    if Sys.file_exists dir then ()
    else let () = make_directory (Filename.dirname dir) in
         if not (Sys.file_exists dir) then
           Sys.mkdir dir 0o777
         else ()

  let make_directory dir =
    try make_directory dir
    with Sys_error err ->
      raise (Sys_error (Printf.sprintf "Failed to create %S (%s)" dir err))

  let with_input_file ?(bin=false) x f =
    let ic = (if bin then open_in_bin else open_in) x in
    Fun.protect ~finally:(fun () -> close_in_noerr ic)
      (fun () -> f ic)

  let file_is_empty filename =
    not (Sys.file_exists filename) ||
    with_input_file filename in_channel_length = 0

  let string_of_file filename =
    with_input_file ~bin:true filename @@ fun chan ->
    let filesize = in_channel_length chan in
    if filesize > Sys.max_string_length then
      failwith
        ("The file " ^ filename ^ " is too large to be loaded into a string")
    else begin
      try really_input_string chan filesize
      with End_of_file ->
        failwith ("Got unexpected end of file while reading " ^ filename)
    end

  let iter_lines_of_file f filename =
    let rec go ic =
      match input_line ic with
      | exception End_of_file -> ()
      | l -> f l; go ic
    in
    with_input_file filename go

  let dump_file oc ?(prefix = "") filename =
    let f s =
      output_string oc prefix; output_string oc s; output_char oc '\n' in
    iter_lines_of_file f filename

  let with_output_file ?(bin=false) x f =
    let oc = (if bin then open_out_bin else open_out) x in
    Fun.protect ~finally:(fun () -> close_out_noerr oc)
      (fun () -> f oc)

  let copy_chan ic oc =
    let m = in_channel_length ic in
    let m = (m lsr 12) lsl 12 in
    let m = Int.max 16384 (Int.min Sys.max_string_length m) in
    let buf = Bytes.create m in
    let rec loop () =
      let len = input ic buf 0 m in
      if len > 0 then begin
        output oc buf 0 len;
        loop ()
      end
    in loop ()

  let copy_file src dest =
    with_input_file ~bin:true src @@ fun ic ->
    with_output_file ~bin:true dest @@ fun oc ->
    copy_chan ic oc

  let rec copy_directory src dst =
    let full_src_path name = Filename.concat src name in
    let full_dst_path name = Filename.concat dst name in
    make_directory dst;
    let content = Array.to_list (readdir src) in
    let is_directory d = is_directory (full_src_path d) in
    let (subdirs, files) = List.partition is_directory content in
    let cp_file name = copy_file (full_src_path name) (full_dst_path name) in
    List.iter cp_file files;
    let cp_dir name =
      copy_directory (full_src_path name) (full_dst_path name)
    in
    List.iter cp_dir subdirs

  let force_remove file =
    if file_exists file then remove file

  let with_chdir path f =
    let oldcwd = Sys.getcwd () in
    Sys.chdir path;
    Fun.protect ~finally:(fun () -> Sys.chdir oldcwd) f

  let getenv_with_default_value variable default_value =
    try Sys.getenv variable with Not_found -> default_value
  let safe_getenv variable = getenv_with_default_value variable ""
end

module Seq = struct
  include Seq

  let rec equal s1 s2 =
    match s1 (), s2 () with
    | Nil, Nil -> true
    | Cons(e1, s1), Cons(e2, s2) -> e1 = e2 && equal s1 s2
    | _, _ -> false
end
