(** All utilities which should be included in all source files are defined here *)

(** {1 Functions from libm} *)

open Constants
open Lacaml.D



external erf_float : float -> float = "erf_float_bytecode" "erf_float"
  [@@unboxed] [@@noalloc]

external erfc_float : float -> float = "erfc_float_bytecode" "erfc_float"
  [@@unboxed] [@@noalloc]

external gamma_float : float -> float = "gamma_float_bytecode" "gamma_float"
  [@@unboxed] [@@noalloc]

external popcnt : int64 -> int32 = "popcnt_bytecode" "popcnt"
  [@@unboxed] [@@noalloc]
  (** popcnt instruction *)

let popcnt i = (popcnt [@inlined] ) i |> Int32.to_int 

external trailz : int64 -> int32 = "trailz_bytecode" "trailz" "int"
  [@@unboxed] [@@noalloc]
  (** ctz instruction *)

let trailz i = trailz i |> Int32.to_int 

external leadz : int64 -> int32 = "leadz_bytecode" "leadz" "int"
  [@@unboxed] [@@noalloc]
  (** bsf instruction *)

external vfork : unit -> int = "unix_vfork" "unix_vfork"

let leadz i = leadz i |> Int32.to_int 


exception SIGTERM

let () = 
  let f _ = raise SIGTERM in
  Sys.set_signal Sys.sigint (Sys.Signal_handle f) 
  ;;


let memo_float_of_int =
  Array.init 64 float_of_int

let float_of_int_fast i =
    if Int.logand i 63 = i then
      memo_float_of_int.(i)
    else
      float_of_int i


let factmax = 150

(* Incomplete gamma function :
  {% $\gamma(\alpha,x) = \int_0^x e^{-t}  t^{\alpha-1} dt$ %}
  {% $p: \frac{1}{\Gamma(\alpha)} \int_0^x e^{-t} t^{\alpha-1} dt$ %} 
  {% $q: \frac{1}{\Gamma(\alpha)} \int_x^\infty e^{-t} t^{\alpha-1} dt$ %} 

  reference - Haruhiko Okumura: C-gengo niyoru saishin algorithm jiten
  (New Algorithm handbook in C language) (Gijyutsu hyouron
  sha, Tokyo, 1991) p.227 [in Japanese] *)

let incomplete_gamma ~alpha x = 
  let a = alpha in
  let a_inv = 1./. a in
  let gf = gamma_float alpha in
  let loggamma_a = log gf in
  let rec p_gamma x =
    if x >= 1. +. a then 1. -. q_gamma x 
    else if x = 0. then 0.
    else
      let rec pg_loop prev res term k =
        if k > 1000. then failwith "p_gamma did not converge."
        else if prev = res then res
        else
          let term = term *. x /. (a +. k) in
          (pg_loop [@tailcall]) res (res +. term) term (k +. 1.)
      in
      let r0 =  exp (a *. log x -. x -. loggamma_a) *. a_inv in
      pg_loop min_float r0 r0 1.

  and q_gamma x =
    if x < 1. +. a then 1. -. p_gamma x 
    else
      let rec qg_loop prev res la lb w k =
        if k > 1000. then failwith "q_gamma did not converge."
        else if prev = res then res
        else
          let k_inv = 1. /. k in
          let kma = (k -. 1. -. a) *. k_inv in
          let la, lb =
            lb, kma *. (lb -. la) +. (k +. x) *. lb *. k_inv
          in
          let w = w *. kma  in
          let prev, res = res, res +. w /. (la *. lb) in
          (qg_loop [@tailcall]) prev res la lb w (k +. 1.)
      in
      let w = exp (a *. log x -. x -. loggamma_a) in
      let lb = (1. +. x -. a) in
      qg_loop min_float (w /. lb) 1. lb w 2.0
  in
  gf *. p_gamma x 





let fact_memo =
  let rec aux accu_l accu = function
    | 0 ->  (aux [@tailcall]) [1.] 1. 1
    | i when (i = factmax) ->
      let x = (float_of_int factmax) *. accu in
      List.rev (x::accu_l)
    | i ->  let x = (float_of_int i) *. accu in
      (aux [@tailcall]) (x::accu_l) x (i+1)
  in
  aux [] 0. 0
  |> Array.of_list



let fact = function
  | i when (i < 0) ->
    raise (Invalid_argument "Argument of factorial should be non-negative")
  | i when (i > 150) ->
    raise (Invalid_argument "Result of factorial is infinite")
  | i -> fact_memo.(i)


let binom =
  let memo = 
    let m = 
      Array.make_matrix 64 64 0 
    in
    for n=0 to Array.length m - 1 do
      m.(n).(0) <- 1;
      m.(n).(n) <- 1;
      for k=1 to (n - 1) do
        m.(n).(k) <- m.(n-1).(k-1) + m.(n-1).(k)
      done
    done;
    m
  in
  let rec f n k =
    assert (k >= 0);
    assert (n >= k);
    if k = 0 || k = n then
      1
    else if n < 64 then 
        memo.(n).(k)
    else
      f (n-1) (k-1) + f (n-1) k
  in f


let binom_float n k =
  binom n k
  |> float_of_int_fast


let rec pow a = function
  | 0 -> 1.
  | 1 -> a
  | 2 -> a *. a
  | 3 -> a *. a *. a
  | -1 -> 1. /. a
  | n when  n > 0  ->
    let b = pow a (n / 2) in
    b *. b *. (if n mod 2 = 0 then 1. else a)
  | n when  n < 0  -> (pow [@tailcall]) (1./.a) (-n)
  | _ -> assert false




let chop f g =
  if (abs_float f) < Constants.epsilon then 0.
  else f *. (g ())



(** Generalized Boys function. 
    maxm : Maximum total angular momentum
    {% $F_m(x) = \frac{\gamma(m+1/2,x)}{2x^{m+1/2}}$ %}
    where %{ $\gamma$ %} is the incomplete gamma function.

    {% $F_0(0.) = 1$ %}
    {% $F_0(t) = \frac{\sqrt{\pi}}{2\sqrt{t}} \text{erf} ( \sqrt{t} )$ %}
    {% $F_m(0.) = \frac{1}{2m+1}$ %}
    {% $F_m(t)  = \frac{\gamma{m+1/2,t}}{2t^{m+1/2}}
    {% $F_m(t)  = \frac{ 2t\, F_{m+1}(t) + e^{-t} }{2m+1}$ %}
*)
let boys_function ~maxm t =
  match maxm with
  | 0 ->
    begin
      if t = 0. then [| 1. |] else
        let sq_t = sqrt t in
        [| (sq_pi_over_two /. sq_t) *.  erf_float sq_t |]
    end
  | _ ->
    begin
      let result =
        Array.init (maxm+1) (fun m -> 1. /. float_of_int (2*m+1))
      in
      let power_t_inv = (maxm+maxm+1)  in
      try  
          let fmax = 
            let t_inv = sqrt (1. /. t) in
            let n = float_of_int maxm in
            let dm = 0.5 +. n in
            let f  = (pow t_inv power_t_inv ) in
            match classify_float f with
              | FP_normal -> (incomplete_gamma dm t) *. 0.5 *. f
              | FP_zero
              | FP_subnormal -> 0.
              | _ -> raise Exit
          in
          let emt = exp (-. t) in
          result.(maxm) <- fmax;
          for n=maxm-1 downto 0 do
            result.(n) <- ( (t+.t) *. result.(n+1) +. emt) *. result.(n)
          done;
          result
      with Exit -> result
    end


let of_some = function
  | Some a -> a
  | None   -> assert false


(** {2 List functions} *)

let list_some l =
  List.filter (function None -> false | _ -> true) l
  |> List.rev_map (function Some x -> x | _ -> assert false)
  |> List.rev


let list_range first last = 
  if last < first then [] else
  let rec aux accu = function
  | 0 -> first :: accu
  | i -> (aux [@tailcall]) ( (first+i)::accu ) (i-1)
  in
  aux [] (last-first)


let list_pack n l =
  assert (n>=0);
  let rec aux i accu1 accu2 = function
  | [] -> if accu1 = [] then
             List.rev accu2
          else 
             List.rev ((List.rev accu1) :: accu2)
  | a :: rest ->
    match i with
    | 0 -> (aux [@tailcall]) (n-1)  [] ((List.rev (a::accu1)) :: accu2) rest
    | _ -> (aux [@tailcall]) (i-1)  (a::accu1) accu2 rest
  in
  aux (n-1) [] [] l


(** {2 Stream functions} *)

let stream_range first last = 
  Stream.from (fun i ->
    let result = i+first in
    if result <= last then
      Some result
    else None
  )

let stream_to_list stream = 
  let rec aux accu =
    let new_accu = 
      try
        Some (Stream.next stream :: accu)
      with Stream.Failure -> None
    in
    match new_accu with
    | Some new_accu -> (aux [@tailcall]) new_accu
    | None -> accu
  in List.rev @@ aux []


let stream_fold f init stream =
  let rec aux accu =
    let new_accu = 
      try
        let element = Stream.next stream in
        Some (f accu element)
    with Stream.Failure -> None
    in  
    match new_accu with
    | Some new_accu -> (aux [@tailcall]) new_accu
    | None -> accu
  in
  aux init

(** {2 Array functions} *)

let array_range first last = 
  if last < first then [| |] else
  Array.init (last-first+1) (fun i -> i+first)


(** {2 Linear algebra} *)


let array_sum a = 
   Array.fold_left ( +. ) 0. a

let array_product a = 
   Array.fold_left ( *. ) 0. a


let diagonalize_symm m_H = 
  let m_V = lacpy m_H in
  let result = 
    syevd ~vectors:true m_V
  in
  m_V, result

let xt_o_x ~o ~x =
  gemm o x
  |> gemm ~transa:`T x

let x_o_xt ~o ~x =
  gemm o x ~transb:`T 
  |> gemm x

let remove_epsilons m =
  let vecs =
    Mat.to_col_vecs m
  in
  Array.map (fun v ->
    let m = abs_float (2. *. amax v) in
    Vec.map (fun x -> if abs_float x < m *. epsilon_float then 0. else x) v
    ) vecs
  |> Mat.of_col_vecs

let canonical_ortho ?thresh:(thresh=1.e-6) ~overlap c =
  let d, u, _ = gesvd (lacpy overlap) in
  let d_sqrt = Vec.sqrt d in
  let n = (* Number of non-negligible singular vectors *)
    Vec.fold (fun accu x -> if x > thresh then accu + 1 else accu) 0 d
  in
  let d_inv_sq = (* D^{-1/2} *)
    Vec.map (fun x ->
      if x >= thresh then 1. /. x
      else 0. ) ~y:d d_sqrt
  in
  if n < Vec.dim d_sqrt then 
    Printf.printf "Removed linear dependencies below %f\n" (1. /. d.{n})
  ;
  Mat.scal_cols u d_inv_sq ;
  gemm c u


let qr_ortho m =
  (** Performed twice for precision *)
  let result = lacpy m in
  let tau = geqrf result in
  orgqr ~tau result;
  let tau = geqrf result in
  orgqr ~tau result;
  result


let normalize v =
  let result = copy v in
  scal (1. /. (nrm2 v)) result;
  result


let normalize_mat m =
  Mat.to_col_vecs m 
  |> Array.map (fun v -> normalize v) 
  |> Mat.of_col_vecs


(** {2 Bitstring functions} *)
let bit_permtutations m n =

  let rec aux k u rest =
    if k=1 then
      List.rev (u :: rest)
    else
      let t   = Z.(logor u (u-one)) in
      let t'  = Z.(t+one) in
      let t'' = Z.(shift_right ((logand (lognot t) t') - one)) (Z.trailing_zeros u + 1) in
      (aux [@tailcall]) (k-1) (Z.logor t' t'') (u :: rest)
  in
  aux (binom n m) Z.(shift_left one m - one) []
      

(** {2 Printers} *)

let pp_float_array_size ppf a = 
  Format.fprintf ppf "@[<2>@[ %d:@[<2>" (Array.length a);
  Array.iter (fun f -> Format.fprintf ppf "@[%10f@]@ " f) a;
  Format.fprintf ppf "]@]@]"

let pp_float_array ppf a = 
  Format.fprintf ppf "@[<2>[@ ";
  Array.iter (fun f -> Format.fprintf ppf "@[%10f@]@ " f) a;
  Format.fprintf ppf "]@]"

let pp_float_2darray ppf a = 
  Format.fprintf ppf "@[<2>[@ ";
  Array.iter (fun f -> Format.fprintf ppf "@[%a@]@ " pp_float_array f) a;
  Format.fprintf ppf "]@]"

let pp_float_2darray_size ppf a = 
  Format.fprintf ppf "@[<2>@[ %d:@[" (Array.length a);
  Array.iter (fun f -> Format.fprintf ppf "@[%a@]@ " pp_float_array_size f) a;
  Format.fprintf ppf "]@]@]"

let pp_matrix ppf m = 
  let open Lacaml.Io in
  let rows = Mat.dim1 m
  and cols = Mat.dim2 m
  in
  let rec aux first last =
    if (first <= last) then begin
        Format.fprintf ppf "@[\n\n    %a@]@ " (Lacaml.Io.pp_lfmat
          ~row_labels:
            (Array.init rows (fun i -> Printf.sprintf "%d  " (i + 1)))
          ~col_labels:
            (Array.init (min 5 (cols-first+1)) (fun i -> Printf.sprintf "-- %d --" (i + first) ))
          ~print_right:false
          ~print_foot:false
          () ) (lacpy ~ac:first ~n:(min 5 (cols-first+1)) m);
        (aux [@tailcall]) (first+5) last
    end
  in
  aux 1 cols


let pp_bitstring n ppf bs =
  String.init n (fun i -> if (Z.testbit bs i) then '+' else '-')
    |> Format.fprintf ppf "@[<h>%s@]"




let string_of_matrix m = 
  Format.asprintf "%a" pp_matrix m

let debug_matrix name a =
  Format.printf "@[%s =\n@[%a@]@]@." name pp_matrix a


let matrix_of_file filename =
  let ic = Scanf.Scanning.open_in filename in
  let rec read_line accu =
    let result =
      try
        Some (Scanf.bscanf ic " %d %d %f" (fun i j v ->
          (i,j,v) :: accu))
      with End_of_file -> None
    in
    match result with
    | Some accu -> (read_line [@tailcall]) accu
    | None      -> List.rev accu
  in
  let data = read_line [] in
  Scanf.Scanning.close_in ic;
  let isize, jsize = 
    List.fold_left (fun (accu_i,accu_j) (i,j,v) ->
      (max i accu_i, max j accu_j)) (0,0) data
  in
  let result =
    Lacaml.D.Mat.of_array
      (Array.make_matrix isize jsize 0.)
  in
  List.iter (fun (i,j,v) -> result.{i,j} <- v) data;
  result


let sym_matrix_of_file filename =
  let result = 
    matrix_of_file filename
  in
  for j=1 to Mat.dim1 result do
    for i=1 to j do
      result.{j,i} <- result.{i,j}
    done;
  done;
  result



let test_case () =

  let test_external () =
     Alcotest.(check (float 1.e-15)) "erf" 0.842700792949715 (erf_float 1.0);
     Alcotest.(check (float 1.e-15)) "erf" 0.112462916018285 (erf_float 0.1);
     Alcotest.(check (float 1.e-15)) "erf" (-0.112462916018285) (erf_float (-0.1));
     Alcotest.(check (float 1.e-15)) "erfc" 0.157299207050285 (erfc_float 1.0);
     Alcotest.(check (float 1.e-15)) "erfc" 0.887537083981715 (erfc_float 0.1);
     Alcotest.(check (float 1.e-15)) "erfc" (1.112462916018285) (erfc_float (-0.1));
     Alcotest.(check (float 1.e-14)) "gamma" (1.77245385090552) (gamma_float 0.5);
     Alcotest.(check (float 1.e-14)) "gamma" (9.51350769866873) (gamma_float (0.1));
     Alcotest.(check (float 1.e-14)) "gamma" (-3.54490770181103) (gamma_float (-0.5));
     Alcotest.(check int) "popcnt" 6 (popcnt @@ Int64.of_int 63);
     Alcotest.(check int) "popcnt" 8 (popcnt @@ Int64.of_int 299605);
     Alcotest.(check int) "popcnt" 1 (popcnt @@ Int64.of_int 65536);
     Alcotest.(check int) "popcnt" 0 (popcnt @@ Int64.of_int 0);
  in
  [
    "External", `Quick, test_external;
  ]

