(*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *)

module U = Unix

let file_exists name =
  try
    U.access name [U.F_OK] ;
    true
  with U.Unix_error _ -> false


(* Forked processes will not start from 0 but this is inessential. *)
let tmp_counter = ref 0

let mktemp base =
  let pid = U.getpid () in
  let rec aux () =
    let name = Printf.sprintf "%s.%d.%08d.tmp" base pid !tmp_counter in
    incr tmp_counter ;
    if
      file_exists name
      (* This should not happen unless the file is very old and the pid is reused. *)
    then aux ()
    else name
  in
  aux ()


let buffer_size = 8192

let tee ic ocs =
  let buffer = Bytes.create buffer_size in
  let rec loop () =
    match input ic buffer 0 buffer_size with
    | 0 ->
        ()
    | r ->
        List.iter (fun oc -> output oc buffer 0 r) ocs ;
        loop ()
  in
  loop ()


let gzip ic oc =
  let ocz = Gzip.open_out_chan oc in
  let buffer = Bytes.create buffer_size in
  let rec loop () =
    match input ic buffer 0 buffer_size with
    | 0 ->
        ()
    | r ->
        Gzip.output ocz buffer 0 r ;
        loop ()
  in
  let success =
    try
      loop () ;
      true
    with Gzip.Error _ -> false
  in
  Gzip.close_out ocz ;
  success


let gunzip ic oc =
  let icz = Gzip.open_in_chan ic in
  let buffer = Bytes.create buffer_size in
  let rec loop () =
    match Gzip.input icz buffer 0 buffer_size with
    | 0 ->
        ()
    | r ->
        output oc buffer 0 r ;
        loop ()
  in
  let success =
    try
      loop () ;
      true
    with Gzip.Error _ -> false
  in
  Gzip.close_in icz ;
  success


let copy ic oc = tee ic [oc]

let rec restart_on_EINTR f x = try f x with U.Unix_error (U.EINTR, _, _) -> restart_on_EINTR f x

let close fd = try U.close fd with U.Unix_error _ -> ()

let close_in = close_in_noerr

let close_out = close_out_noerr

let wait pid =
  match snd (restart_on_EINTR (U.waitpid []) pid) with U.WEXITED 0 -> true | _ -> false


let exec args stdin stdout stderr =
  wait
    (U.create_process args.(0) args (U.descr_of_in_channel stdin) (U.descr_of_out_channel stdout)
       (U.descr_of_out_channel stderr))


let diff file1 file2 oc = exec [|"diff"; file1; file2|] stdin oc stderr

let fork f =
  let fd_in, fd_out = U.pipe () in
  match U.fork () with
  | 0 -> (
      U.close fd_in ;
      try
        if f (U.out_channel_of_descr fd_out) then exit 0
        else (
          close fd_out ;
          exit 1 )
      with _ ->
        close fd_out ;
        exit 2 )
  | pid ->
      if pid < 0 then failwith "fork error"
      else (
        U.close fd_out ;
        (pid, U.in_channel_of_descr fd_in) )


let compose f g ic oc =
  let pid, ic1 = fork (f ic) in
  let r1 = g ic1 oc in
  let r2 = wait pid in
  close_in ic1 ;
  r1 && r2


let diff_on_same_input f1 f2 ic oc =
  let file = mktemp "input" in
  let ofile = open_out file in
  copy ic ofile ;
  close_out ofile ;
  let ifile1 = open_in file and ifile2 = open_in file in
  let file1 = mktemp "output1" and file2 = mktemp "output2" in
  let ofile1 = open_out file1 and ofile2 = open_out file2 in
  let r1 = f1 ifile1 ofile1 and r2 = f2 ifile2 ofile2 in
  close_in ifile1 ;
  close_in ifile2 ;
  close_out ofile1 ;
  close_out ofile2 ;
  let success = if r1 && r2 then diff file1 file2 oc else false in
  U.unlink file ;
  U.unlink file1 ;
  U.unlink file2 ;
  success