open Util
open Constants

module Am   = AngularMomentum
module Asp  = AtomicShellPair
module Aspc = AtomicShellPairCouple
module Co   = Coordinate
module Cs   = ContractedShell
module Csp  = ContractedShellPair
module Cspc = ContractedShellPairCouple
module Po   = Powers
module Psp  = PrimitiveShellPair
module Pspc = PrimitiveShellPairCouple
module Ps   = PrimitiveShell

let cutoff = Constants.integrals_cutoff 
let cutoff2 = cutoff *. cutoff

exception NullQuartet

type four_idx_intermediates =
  {
    zero         : float array array;
    gp           : float array;
    gg           : float array;
    gq           : float array;
    coef_g       : float array ;
    center_ra    : Co.t array ;
    center_rc    : Co.t array ;
    center_ab    : Co.t  ;
    center_cd    : Co.t  ;
  }

(** Horizontal and Vertical Recurrence Relations (HVRR) *)
let rec hvrr angMom_a angMom_b angMom_c angMom_d
    abcd map_x map_y map_z =

  let gp   = abcd.gp   in
  let gg   = abcd.gg   in
  let gq   = abcd.gq   in

  let coef_g = abcd.coef_g in

  let run xyz map =

    let zero =
      match xyz with
      | Co.X -> abcd.zero.(0)
      | Co.Y -> abcd.zero.(1)
      | Co.Z -> abcd.zero.(2)
    in
    let angMom_a  = Po.get xyz angMom_a in
    let angMom_b  = Po.get xyz angMom_b in
    let angMom_c  = Po.get xyz angMom_c in
    let angMom_d  = Po.get xyz angMom_d in
    let center_ab = Co.get xyz abcd.center_ab in
    let center_cd = Co.get xyz abcd.center_cd in
    let center_ra = Array.map (fun x -> Co.get xyz x) abcd.center_ra in
    let center_rc = Array.map (fun x -> Co.get xyz x) abcd.center_rc in

    let rec vrr angMom_a angMom_c = 
              match angMom_a, angMom_c with
              | 0, -1
              | -1, 0 -> assert false
              | 0, 0 -> zero
              | 1, 0 -> 
                let v1 = zero in
                Array.mapi (fun i v1i -> center_ra.(i) *. v1i ) v1
              | 0, 1 -> 
                let v1 = zero in
                Array.mapi (fun i v1i -> center_rc.(i) *.  v1i ) v1
              | 1, 1 -> 
                let v1 = vrr 1 0 in
                let v2 = zero in
                Array.mapi (fun i v1i -> center_rc.(i) *.  v1i +.
                  gg.(i) *. v2.(i) ) v1
              | 2, 0 -> 
                let v1 = vrr 1 0 in
                let v2 = zero in
                Array.mapi (fun i v1i -> center_ra.(i) *. v1i +. gp.(i) *. v2.(i)) v1
              | _, 0 -> 
                let v1 = 
                  vrr (angMom_a-1) 0
                in
                let a = float_of_int_fast (angMom_a-1) in
                let v2 = 
                  vrr (angMom_a-2) 0
                in
                Array.mapi (fun i v1i -> center_ra.(i) *. v1i +. a *. gp.(i) *. v2.(i)) v1
              | _, 1 -> 
                let v1 = 
                  vrr angMom_a 0
                in
                let a = float_of_int_fast angMom_a in
                let v2 = 
                  vrr (angMom_a-1) 0
                in
                Array.mapi (fun i v1i -> center_rc.(i) *.  v1i +.
                  a *. gg.(i) *. v2.(i) ) v1
              | 0, _ -> 
                let v1 = 
                  vrr 0 (angMom_c-1)
                in
                let b = float_of_int_fast (angMom_c-1) in
                let v3 = 
                  vrr 0 (angMom_c-2)
                in
                Array.mapi (fun i v1i -> center_rc.(i) *.  v1i +.
                  b *. gq.(i) *. v3.(i)) v1
              | _ -> 
                let v1 = 
                  vrr angMom_a (angMom_c-1)
                in
                let a = float_of_int_fast angMom_a in
                let b = float_of_int_fast (angMom_c-1) in
                let v2 = 
                  vrr (angMom_a-1) (angMom_c-1)
                in
                let v3 = 
                  vrr angMom_a (angMom_c-2)
                in
                Array.mapi (fun i v1i -> center_rc.(i) *.  v1i +.
                a *. gg.(i) *. v2.(i) +.  b *. gq.(i) *. v3.(i)) v1
    in

    let rec hrr angMom_a angMom_b angMom_c angMom_d =
      let key = Zkey.of_int_four angMom_a angMom_b angMom_c angMom_d in

      try Zmap.find map key with
      | Not_found -> 
        let result = 
            match angMom_b, angMom_d with
            | -1, 0 
            | 0, -1 -> assert false 
            | 0, 0 ->
              vrr angMom_a angMom_c
            | _, 0 -> 
              let h1 = 
                hrr (angMom_a+1) (angMom_b-1) angMom_c angMom_d 
              in
              if center_ab = 0. then
                h1
              else
                let h2 = 
                  hrr angMom_a (angMom_b-1) angMom_c angMom_d 
                in
                Array.mapi (fun i h1i -> h1i +. center_ab *. h2.(i)) h1
            | _ -> 
              let h1 = 
                hrr angMom_a angMom_b (angMom_c+1) (angMom_d-1)
              in
              if center_cd = 0. then
                h1
              else
                let h2 = 
                  hrr angMom_a angMom_b angMom_c (angMom_d-1) 
                in
                Array.mapi (fun i h1i -> h1i +. center_cd *. h2.(i)) h1
        in (Zmap.add map key result; result)

      in
      hrr angMom_a angMom_b angMom_c angMom_d
    in
    let x, y, z = 
      (run Co.X map_x), (run Co.Y map_y), (run Co.Z map_z)
    in
    let rec aux accu = function
      | 0 -> accu +. coef_g.(0) *. x.(0) *. y.(0) *. z.(0)
      | i -> (aux [@tailcall]) (accu +. coef_g.(i) *. x.(i) *. y.(i) *. z.(i)) (i-1)
    in
    aux 0. (Array.length x - 1)


let contracted_class_shell_pair_couple ~basis expo_g_inv coef_g shell_pair_couple : float Zmap.t =

    (* Pre-computation of integral class indices *)
    let class_indices = Cspc.zkey_array shell_pair_couple in

    let contracted_class = 
      Array.make (Array.length class_indices) 0.;
    in

    (* Compute all integrals in the shell for each pair of significant shell pairs *)

    let shell_p = Cspc.shell_pair_p shell_pair_couple
    and shell_q = Cspc.shell_pair_q shell_pair_couple
    in

    let center_ab = Csp.a_minus_b shell_p
    and center_cd = Csp.a_minus_b shell_q
    in

    let norm_scales = Cspc.norm_scales shell_pair_couple in

    List.iter (fun (coef_prod, spc) ->

        let sp_ab = Pspc.shell_pair_p spc
        and sp_cd = Pspc.shell_pair_q spc
        in

        let expo_p_inv = Psp.exponent_inv sp_ab in
        let expo_q_inv = Psp.exponent_inv sp_cd in
        let expo_pgq = Array.map (fun e -> 
          1. /. (expo_p_inv +. expo_q_inv +. e)
          ) expo_g_inv
        in

        let fp = Array.map (fun e -> expo_p_inv *. e) expo_pgq in
        let fq = Array.map (fun e -> expo_q_inv *. e) expo_pgq in

        let gp =
          let x = 0.5 *. expo_p_inv in
          Array.map (fun f -> (1. -. f) *. x) fp
        in
        let gq =
          let x = 0.5 *. expo_q_inv in
          Array.map (fun f -> (1. -. f) *. x) fq
        in
        let gg =
          let x = 0.5 *. expo_q_inv in
          Array.map (fun f -> f *. x) fp
        in

        let center_pq = Co.(Psp.center sp_ab |- Psp.center sp_cd) in
        let center_qc = Psp.center_minus_a sp_cd in
        let center_pa = Psp.center_minus_a sp_ab in

        let center_ra = Array.map (fun f -> Co.(center_pa |- (f |. center_pq))) fp in
        let center_rc = Array.map (fun f -> Co.(center_qc |+ (f |. center_pq))) fq in

        (* zero_m is defined here *)
        let zero = Array.map (fun xyz ->
            let x = Co.get xyz center_pq in
            Array.mapi (fun i ei ->
                let fg = expo_g_inv.(i) *. ei in
                (sqrt fg) *. exp (-. x *. x *. ei )) expo_pgq
          ) Co.[| X ; Y ; Z |]
        in
        begin
          match Cspc.ang_mom shell_pair_couple with
          (*
          | Am.S ->
            let integral =
              zero.(0) *. zero.(1) *. zero.(2)
            in
            contracted_class.(0) <- contracted_class.(0) +. coef_prod *. integral 
            *)
          | _ -> 
            let map_x, map_y, map_z =
              Zmap.create (Array.length class_indices),
              Zmap.create (Array.length class_indices),
              Zmap.create (Array.length class_indices)
            in
            (* Compute the integral class from the primitive shell quartet *)
            class_indices
            |> Array.iteri (fun i key ->
                let (angMom_a,angMom_b,angMom_c,angMom_d) =
                  match Zkey.to_powers key with
                  | Zkey.Twelve x -> x
                  | _             -> assert false
                in
                let norm =  norm_scales.(i) in
                let coef_prod = coef_prod *. norm in
                let abcd = {
                  zero ; gp ; gg ; gq ; 
                  center_ab ; center_cd ; 
                  center_ra ; center_rc ;
                  coef_g ;
                } in
                let integral = 
                  hvrr
                    angMom_a angMom_b angMom_c angMom_d
                    abcd map_x map_y map_z
                in
                contracted_class.(i) <- contracted_class.(i) +. coef_prod *. integral 
              )
        end
      ) (Cspc.coefs_and_shell_pair_couples shell_pair_couple);

    let result = 
      Zmap.create (Array.length contracted_class)
    in


    Array.iteri (fun i key -> Zmap.add result key contracted_class.(i)) class_indices;
    result



