open Lacaml.D
open Util
open Constants

(** One-electron orthogonal basis set, corresponding to Molecular Orbitals. *)

module HF = HartreeFock
module Si = Simulation

type mo_type =
  | RHF | ROHF | UHF | CASSCF | Projected
  | Natural of string
  | Localized of string

type t =
  {
    simulation      : Simulation.t;      (* Simulation which produced the MOs *)
    mo_type         : mo_type;           (* Kind of MOs (RHF, CASSCF, Localized...) *)
    mo_occupation   : Vec.t;             (* Occupation numbers *)
    mo_coef         : Mat.t;             (* Matrix of the MO coefficients in the AO basis *)
    eN_ints         : NucInt.t lazy_t;   (* Electron-nucleus potential integrals *)
    ee_ints         : ERI.t lazy_t;      (* Electron-electron potential integrals *)
    f12_ints        : F12.t lazy_t;      (* F12 integrals *)
    kin_ints        : KinInt.t lazy_t;   (* Kinetic energy integrals *)
    one_e_ints      : Mat.t lazy_t;      (* Kinetic energy integrals *)
  }


let size t =
  Mat.dim2 t.mo_coef

let simulation    t = t.simulation
let mo_type       t = t.mo_type
let ao_basis      t = Si.ao_basis t.simulation
let mo_occupation t = t.mo_occupation
let mo_coef       t = t.mo_coef
let eN_ints       t = Lazy.force t.eN_ints
let ee_ints       t = Lazy.force t.ee_ints
let kin_ints      t = Lazy.force t.kin_ints
let two_e_ints    t = Lazy.force t.ee_ints
let f12_ints      t = Lazy.force t.f12_ints
let one_e_ints    t = Lazy.force t.one_e_ints


let mo_energies t =
  let m_C = mo_coef t in
  let f =
    let m_N = Mat.of_diag @@ mo_occupation t in
    let m_P = x_o_xt m_N m_C in
    match t.mo_type with
    | RHF  -> Fock.make_rhf  ~density:m_P (ao_basis t)
    | Projected
    | ROHF -> (Mat.scal 0.5 m_P;
               Fock.make_uhf  ~density_same:m_P ~density_other:m_P (ao_basis t))
    | _ -> failwith "Not implemented"
  in
  let m_F0 = Fock.fock f in
  xt_o_x m_F0 m_C
  |> Mat.copy_diag


let mo_matrix_of_ao_matrix ~mo_coef ao_matrix =
  xt_o_x ~x:mo_coef ~o:ao_matrix


let ao_matrix_of_mo_matrix ~mo_coef ~ao_overlap mo_matrix =
  let sc = gemm ao_overlap mo_coef  in
  x_o_xt ~x:sc ~o:mo_matrix


let make ~simulation ~mo_type ~mo_occupation ~mo_coef ()  =
  let ao_basis =
    Si.ao_basis simulation
  in
  let eN_ints = lazy (
    AOBasis.eN_ints ao_basis
    |> NucInt.matrix
    |> mo_matrix_of_ao_matrix ~mo_coef
    |> NucInt.of_matrix
  )
  and kin_ints = lazy (
    AOBasis.kin_ints ao_basis
    |> KinInt.matrix
    |> mo_matrix_of_ao_matrix ~mo_coef
    |> KinInt.of_matrix
  )
  and ee_ints = lazy (
    AOBasis.ee_ints ao_basis
    |> ERI.four_index_transform mo_coef
  )
  and f12_ints = lazy (
    AOBasis.f12_ints ao_basis
    |> F12.four_index_transform mo_coef
  )
  in
  let one_e_ints = lazy (
    Mat.add (NucInt.matrix @@ Lazy.force eN_ints)
        (KinInt.matrix @@ Lazy.force kin_ints) )
  in
  { simulation ; mo_type ; mo_occupation ; mo_coef ;
    eN_ints ; ee_ints ; kin_ints ; one_e_ints ;
    f12_ints }


let values t point =
  let c = mo_coef t in
  let a = AOBasis.values (Simulation.ao_basis t.simulation) point in
  gemv ~trans:`T c a

let of_hartree_fock hf =
  let mo_coef       = HF.eigenvectors hf in
  let simulation    = HF.simulation   hf in
  let mo_occupation = HF.occupation   hf in
  let mo_type =
    match HF.kind hf with
    | HartreeFock.RHF  -> RHF
    | HartreeFock.ROHF -> ROHF
    | HartreeFock.UHF  -> UHF
  in
  make ~simulation ~mo_type ~mo_occupation ~mo_coef ()


let of_mo_basis simulation other =

  let mo_coef =
    let basis = Simulation.ao_basis simulation in
    let basis_other = ao_basis other in
    let m_S =
      Overlap.(matrix @@ of_basis_pair
        (AOBasis.basis basis)
        (AOBasis.basis basis_other) )
    in
    let m_X = AOBasis.ortho basis in
    (* Project other vectors in the current basis *)
    let m_C =
      gemm m_S @@ mo_coef other
    in
    (* Append dummy vectors to the input vectors *)
    let result =
      let vecs = Mat.to_col_vecs m_X in
      Array.iteri (fun i v -> if (i < Array.length vecs) then vecs.(i) <- v)
	(Mat.to_col_vecs m_C) ;
      Mat.of_col_vecs vecs
    in
    (* Gram-Schmidt Orthonormalization *)
    gemm m_X @@ (Util.qr_ortho @@ gemm ~transa:`T m_X result)
    |> Util.remove_epsilons
    |> Conventions.rephase
  in

  let mo_occupation =
    let occ = mo_occupation other in
    Vec.init (Mat.dim2 mo_coef) (fun i ->
      if (i <= Vec.dim occ) then occ.{i}
      else 0.)
  in
  make ~simulation ~mo_type:Projected ~mo_occupation ~mo_coef ()





let pp ?(start=1) ?(finish=0) ppf t =
    let open Lacaml.Io in
    let rows = Mat.dim1 t.mo_coef
    and cols = Mat.dim2 t.mo_coef
    in
    let finish =
      match finish with
      | 0 -> cols
      | x -> x
    in

    let rec aux first =

      if (first > finish) then ()
      else
        begin
          Format.fprintf ppf "@[<v>@[<v4>@[<h>%s@;" "Eigenvalues:";

          Array.iteri (fun i x ->
              if (i+1 >= first) && (i+1 <= first+4 ) then
                Format.fprintf ppf "%12f@ " x)
            (Vec.to_array @@ mo_energies t);

          Format.fprintf ppf "@]@;";
          Format.fprintf ppf "@[%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)) (t.mo_coef)) ;
          Format.fprintf ppf "@]@;@;@]";
          (aux [@tailcall]) (first+5)
        end
    in
    aux start


