/*
 * Copyright University of Reims Champagne-Ardenne
 * Authors and Contributors: Akilan RAJAMANI, Corentin LEFEBVRE, Johanna KLEIN,
 *                           Emmanuel PLUOT, Gaetan RUBEZ, Hassan KHARTABIL,
 *                           Jean-Charles BOISSON and Eric HENON
 * (24/07/2017)
 * jean-charles.boisson@univ-reims.fr, eric.henon@univ-reims.fr
 *
 * This software is a computer program whose purpose is to
 * detect and quantify interactions from electron density
 * obtained either internally from promolecular density or
 * calculated from an input wave function input file. It also
 * prepares for the visualization of isosurfaces representing
 * several descriptors (dg) coming from the IGM methodology.
 *
 * This software is governed by the CeCILL-C license under French law and
 * abiding by the rules of distribution of free software.  You can  use,
 * modify and/ or redistribute the software under the terms of the CeCILL-C
 * license as circulated by CEA, CNRS and INRIA at the following URL
 * "http://www.cecill.info".
 *
 * As a counterpart to the access to the source code and  rights to copy,
 * modify and redistribute granted by the license, users are provided only
 * with a limited warranty  and the software's author,  the holder of the
 * economic rights,  and the successive licensors  have only  limited
 * liability.
 *
 * In this respect, the user's attention is drawn to the risks associated
 * with loading,  using,  modifying and/or developing or reproducing the
 * software by the user in light of its specific status of free software,
 * that may mean  that it is complicated to manipulate,  and  that  also
 * therefore means  that it is reserved for developers  and  experienced
 * professionals having in-depth computer knowledge. Users are therefore
 * encouraged to load and test the software's suitability as regards their
 * requirements in conditions enabling the security of their systems and/or
 * data to be ensured and,  more generally, to use and operate it in the
 * same conditions as regards security.
 *
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL-C license and that you accept its terms.
 *
 * */

/**
 * @file Node.cpp
 * @brief Implementation of Node.h
 * @author Emmanuel */

#ifndef _NODE_CPP_
#define _NODE_CPP_

// LOCAL
#include <Node.h>

Node::Node(int nbAtomParam, int nbAtomMolAParam, ProgData &pdataParam):nbAtom(nbAtomParam), nbAtomMolA(nbAtomMolAParam), pdata(pdataParam)
{
			
  lol2=0;
  /* Memory allocation */
  diffX 			= new double[nbAtom];
  diffY 			= new double[nbAtom];
  diffZ 			= new double[nbAtom];
			
  squareX 		= new double[nbAtom];
  squareY 		= new double[nbAtom];
  squareZ	 	= new double[nbAtom];
			
  rho1 			= new double[nbAtom];
  rho2 			= new double[nbAtom];
  rho3			= new double[nbAtom];

  opti                  = new double[nbAtom];
		
  distance 		= new double[nbAtom];
			
  /* Local data instanciation */
  data = new LocalData( nbAtomMolA, nbAtom );
			
}
		
Node::~Node()
{
			
  /* Memory deallocation */	
  delete [] diffX;
  delete [] diffY;
  delete [] diffZ;
			
  delete [] squareX;
  delete [] squareY;
  delete [] squareZ;
			
  delete [] rho1;
  delete [] rho2;
  delete [] rho3;

  delete [] opti;
		
  delete [] distance;

  /* Data destruction */
  delete data;
				
}
	
void
Node::computeDensity(axis_t & __restrict posGrid, param_t & __restrict params)
{
			
			
  /* Initilization */
  rho		=	0.0;
  contribution	=	0.0;

			
  /* Looping through the atoms in the first FRAGMENT */
  for( int j(0) ; j < nbAtomMolA ; ++j ){
				
    int atomType = pdata.atomTypes[j];
    double pos[3] = {pdata.atomPositions.xValues[j], pdata.atomPositions.yValues[j], pdata.atomPositions.zValues[j]};
				
    /* Setting differences */
    diffX[j]	= ( posGrid.x - pos[X] );
    diffY[j]	= ( posGrid.y - pos[Y] );
    diffZ[j]	= ( posGrid.z - pos[Z] );
				
    /* Processing differences' squares */
    squareX[j]	= ( diffX[j] * diffX[j] );
    squareY[j]	= ( diffY[j] * diffY[j] );
    squareZ[j]	= ( diffZ[j] * diffZ[j] );
				
    /* Distance processing */
    distance[j]	= std::sqrt( squareX[j] + squareY[j] + squareZ[j] );
	
    /* Setting R_TRESHOLD as minimum */
    distance[j] = ( distance[j] < R_TRESHOLD ? R_TRESHOLD : distance[j] );
	
    /* Rho processing */
    /* as the sum of three exponential contributions (not appropriate notation X,Y,Z here*/
    rho1[j]= A1[ atomType ]	*	std::exp( -B1[ atomType ] * distance[j] );
    rho2[j]= A2[ atomType ]	*	std::exp( -B2[ atomType ] * distance[j] );
    rho3[j]= A3[ atomType ]	*	std::exp( -B3[ atomType ] * distance[j] );
    rho   += rho1[j] + rho2[j] + rho3[j]; 
				
  }
			
  /* Setting contribution as the rho for the first molecule */
  contribution = rho;

			
  /* Looping through the atoms in the second FRAGMENT */
  for( int j(nbAtomMolA) ; j < nbAtom ; ++j){
				
    int atomType = pdata.atomTypes[j];
    double pos[3] = {pdata.atomPositions.xValues[j], pdata.atomPositions.yValues[j], pdata.atomPositions.zValues[j]};
				
    /* Setting differences */
    diffX[j]	= ( posGrid.x - pos[X] );
    diffY[j]	= ( posGrid.y - pos[Y] );
    diffZ[j]	= ( posGrid.z - pos[Z] );
				
    /* Processing differenes' squares */
    squareX[j]	= ( diffX[j] * diffX[j] );
    squareY[j]	= ( diffY[j] * diffY[j] );
    squareZ[j]	= ( diffZ[j] * diffZ[j] );
				
    /* Distance processing */
    distance[j]	= std::sqrt( squareX[j] + squareY[j] + squareZ[j] );
	
    /* Setting R_TRESHOLD as minimum */
    distance[j] = ( distance[j] < R_TRESHOLD ? R_TRESHOLD : distance[j] );
	
    /* Rho processing */
    rho1[j]= A1[ atomType ]	*	std::exp( -B1[ atomType ] * distance[j] );
    rho2[j]= A2[ atomType ]	*	std::exp( -B2[ atomType ] * distance[j] );
    rho3[j]= A3[ atomType ]	*	std::exp( -B3[ atomType ] * distance[j] );
    rho   += rho1[j] + rho2[j] + rho3[j];
	
  }
			
  /* Deciding whether the computing should continue or not depending on the contribution and rho */
  // In the original NCIPlot code it is possible to discard the grid nodes for which more than 
  // a fraction (default threshold value is 0.95) of the total promolecular density comes 
  // from only one molecule (A or B). Typically, this turns off the intramolecular interactions 
  // in the resulting files for NCI calculations only. IGM result files won’t be affected by this parameter.
  contribution /= rho;
  shouldCompute = (contribution <= params.intermolecular ) && (contribution >= (1-params.intermolecular));
			
}

void
Node::computeGradHess()
{
	
  //Initializing //
  data->init();
  hess[0]=0.0;
  hess[1]=0.0;
  hess[2]=0.0;
  hess[3]=0.0;
  hess[4]=0.0;
  hess[5]=0.0;

  //double B1xRhoX,B2yRhoY,B3zRhoZ;
  
  for(int i = 0 ; i < nbAtom ; ++i){

    /* Computing the multiplicative inverse and its square */
    multInvR 			= 	1.0 / distance[i];
    squareMultInvR 		= 	multInvR * multInvR;

    /* Summing rho * B = first derivative of the atomic density */
    sumBRho = B1[pdata.atomTypes[i]] * rho1[i] + B2[pdata.atomTypes[i]] * rho2[i] + B3[pdata.atomTypes[i]] * rho3[i];

    /* Summing rho * B * B : contribution to the second derivative */ 
    sumSquareBRho		= 	B1[pdata.atomTypes[i]] * B1[pdata.atomTypes[i]] * rho1[i] 
                                      + B2[pdata.atomTypes[i]] * B2[pdata.atomTypes[i]] * rho2[i] 
                                      + B3[pdata.atomTypes[i]] * B3[pdata.atomTypes[i]] * rho3[i];
				
    /* Operation used three times */
    opti[i] = multInvR * sumBRho; // opti will serve for another calculation after basis change
				
    /* Processing partial ATOMIC gradients */
    partialGradient.x 	= 	-diffX[i] * opti[i];
    partialGradient.y 	= 	-diffY[i] * opti[i];
    partialGradient.z 	=	-diffZ[i] * opti[i];

    /* Updating molecular gradient value (grad, gradIGM) with current partial gradient values */
   /*  Also, incorporate the current ATOMIC contribution i to the gradient expression at current grid point */ 
   /* and !! STORE the current ATOMIC gradient in ARRAY: contributionAtomicAlone */ 
   /* contributionAtomicAlone[i].x = partialGrad.x; */
   // also: store the contribution of each fragment : contributionInsideMoleculeA (and B)
    data->updateGrad(i, partialGradient); // --> the binary partition is made here

				
    // Updating hessian diagonal values //
    hess[0]+=	squareMultInvR * ( (squareX[i] * multInvR - distance[i]) * sumBRho + squareX[i] * sumSquareBRho);
    hess[3]+=	squareMultInvR * ( (squareY[i] * multInvR - distance[i]) * sumBRho + squareY[i] * sumSquareBRho);
    hess[5]+=	squareMultInvR * ( (squareZ[i] * multInvR - distance[i]) * sumBRho + squareZ[i] * sumSquareBRho);

    // Operation used three times //
    opti2  = opti[i] + sumSquareBRho;
    opti2 *= squareMultInvR;
				
    // Updating hessian off-diagonal values //
    hess[1]				+=	diffX[i] * diffY[i] * opti2;
    hess[2]				+=	diffX[i] * diffZ[i] * opti2;
    hess[4]				+=	diffY[i] * diffZ[i] * opti2;
  } // end of sum over atoms

  // once contributions of fragments A and B have been estimated, 
  // one can calculate the ED grad IGMInter components:
  data->updateIGMInter();

} // end of computeGradHess()


void
Node::reComputeGrad()
{ // compute the gradient in the new reference frame (after basis change)
  // ,only after having computed the ED gradient once in the original repear
  // then we can re-use some elements previously computed (like sumBrho/r)
	
  /*Initializing */
  data->reinit();

  for(int i = 0 ; i < nbAtom ; ++i){

    // Processing partial ATOMIC gradients: with basis change, since diffX,Y,Z have previously been recomputed!
    partialGradient.x 	= 	-diffX[i] * opti[i]; // we re-use opti already computed, which does not depend on repear
    partialGradient.y 	= 	-diffY[i] * opti[i];
    partialGradient.z 	=	-diffZ[i] * opti[i];

    /* Updating molecular gradient value (grad, gradIGM) with current partial gradient values */
   /*  Also, incorporate the current ATOMIC contribution i to the gradient expression at current grid point */ 
   /* and !! STORE the current ATOMIC gradient in ARRAY: contributionAtomicAlone */ 
   /* contributionAtomicAlone[i].x = partialGrad.x; */
   // also: store the contribution of each fragment : contributionInsideMoleculeA (and B)
    data->updateGrad(i, partialGradient); // --> the binary partition is made here

				
  } // end of sum over atoms

  // once contributions of fragments A and B have been estimated (in update), 
  // one can calculate the ED grad IGMInter components:
  data->updateIGMInter();

} // end of reComputeGrad()


void
Node::sortLambdas()
{

  if( lambdas[0]<=lambdas[1] ){
			
    tmp	= lambdas[0];
				
    if( lambdas[1]<lambdas[2] ){
				
      lambdas[0]	= lambdas[2];
      lambdas[2]	= tmp;
					
    }else{
				
      lambdas[0]	= lambdas[1];
      lambdas[1]	= tmp;
					
    }
				
  }
  if( lambdas[1]<lambdas[2] ){
			
    tmp		= lambdas[1];
    lambdas[1] 	= lambdas[2];
    lambdas[2] 	= tmp;
				
  }                        

} // end of sortLambdas

void
Node::computeSortLambdas()
{
// we need heigs and eigenvectors (for basis change)
   this->diagonalizeHessian();

// CARDAN's method
// hess = double hess[6] = static array
/*			
  a = -(hess[0] + hess[3] +hess[5]);

  b = hess[0] * hess[3] + hess[0] * hess[5] + hess[3] * hess[5] - hess[1] * hess[1] - hess[2] * hess[2] - hess[4] * hess[4];

  c = hess[0] * hess[4] * hess[4] + 
      hess[3] * hess[2] * hess[2] + 
      hess[5] * hess[1] * hess[1] - 
      hess[0] * hess[3] * hess[5] - 
      2 * hess[1] * hess[2] * hess[4];

  // Keeping 1/3 * a as the value is used five times 
  opti = a * ONE_THIRD;

  p 			= 	b -	a * opti;
  q 			= 	a *	a *	a *	0.0740740740 -	b *	opti + c;

  determinant =	q * q + p * p * p * 0.148148148148148148;

  if(determinant < 0.0){
			
    // These values are used three times 
    opti2 = std::sqrt( -p * ONE_THIRD ) * 2.0;
    opti3 = std::acos(3*q/(2*p) * std::sqrt(-3 / p));
				
    lambdas[0] =  opti2 * std::cos(     opti3	  * ONE_THIRD) 	- opti;
    lambdas[1] =  opti2 * std::cos( (opti3 + PI_2) * ONE_THIRD)	- opti;
    lambdas[2] =  opti2 * std::cos( (opti3 + PI_4) * ONE_THIRD)	- opti;
*/

    // Sorting lambdas 
    //sortLambdas();
  //}
}
		
void
Node::process( axis_t & posGrid, int index, Results &results, param_t & params )
{
   // posGrid = input = x,y,z of current examined grid point
   // index   = input = position of current point in the grid represented as a one dimension array
   //                   the loop order is:  do iz=0 -> nbstep(2)
   //                                        do iy=0 -> nbstep(1)
   //                                         do ix=0 -> nbstep(0)
   // ==> inner loop is on X axis  ==> index = iz *[nbstep(2) * nbstep(1)] + iy * [nbstep(1)] + ix !!!
   //                                        = number of elements before the current examined grid point
   // results : a reference pointing towards the Result object of NCISolver
   // params  : the parameters read in param.igm
   //
  /* Computing the total density at the current node */



  computeDensity(posGrid, params);

  /* store the result of checking if the point is interesting */
  /* Deciding whether the computing should continue or not depending on the contribution and rho */
  // In the original NCIPlot code it is possible to discard the grid nodes for which more than 
  // a fraction (default threshold value is 0.95) of the total promolecular density comes 
  // from only one molecule (A or B). Typically, this turns off the intramolecular interactions 
  // in the resulting files for NCI calculations only. IGM result files won’t be affected by this parameter.

  results.intermolecular(shouldCompute, index);
		
  /* Computing gradient, IGM gradient, IGMInter gradient and hessian values */
  /* and store tha ATOMIC contribution in data->contributionAtomicAlone[i].x,y,z */
  computeGradHess();
			
  /* Computing and sorting lambdas */
  //computeSortLambdas();  --> DEPRECATED (CARDAN method has been replaced by TQL iterative method)
  diagonalizeHessian();

  /* DEPRECATED , for CARDAN method: Checks if determinant in inferior to 0 
  if( determinant < 0.0 ){ */

			
  /* Vectors' norms calculation */
  /* just compute the norm ... */
  data->processNormGrad();         // sqrt( gradx*gradx + ...)
  data->processNormGradIGM();      // sqrt( gradIGMx*gradIGMx + ..)
  data->processNormGradIGMInter(); // sqrt( gradIGMInterx*gradIGMInterx + ...)

  // STORE RESULTS in results object				
  // Updating cube for current node with:
  // x deltag INTRA  
  // x deltag INTER 
  // x rho
  // x RDG
  // FOR isosurface building or building final .dat file
  results.updateCube(index, *data, rho, lambdas[1]); // values to build the cube files (isosurfaces)

  // ========================================================================================
  // R E O R I E N T A T I O N   i n    t h e    l o c a l     r e f e r e n c e    F R A M E 
  // ========================================================================================
  // To have scores rotational independent
  // Change the orientation of the vectors primitive gradients and dx,dy,dz in the new reference frame
  // defined by the eigenvector of the ED hessien
  
  // basis change for the density gradient vector
     double matrixHessianEigVec[3][3];

  // all eigenvectors are orthonormal ==> the change-of-basis matrix to go 
  // from the XYZ reference frame to the ED Curvature reference frame
  // is the transposed matrix (i.e. the inverse here) of the matrix formed with the
  // 3 eigenVect and is called now: matrixHessianEigVec
     getTransposed(eigV, matrixHessianEigVec); 

  // basis change of diffx (diffy, diffz)  vectors stored in the Node structure 
     basisChange(matrixHessianEigVec);
     reComputeGrad(); // only the gradient components have to be recomputed (after reinitialization of old values)

  /* Vectors' norms calculation */
  /* just compute the norm ... */
  data->processNormGrad();         // sqrt( gradx*gradx + ...)
  data->processNormGradIGM();      // sqrt( gradIGMx*gradIGMx + ..)
  data->processNormGradIGMInter(); // sqrt( gradIGMInterx*gradIGMInterx + ...)

  // STORE RESULTS in results object                            
  // Updating cube for current node with:
  // x deltag INTRA  
  // x deltag INTER 
  results.updateCubeFC(index, *data); // save values to build the cube files for scoring integration

				
     /* ========= A T O M I C   D E C O M P O S I T I O N   ========== */
     /*       of intermolecular interactions between two framents      */
     /* ============================================================== */
    /* Looping through focused atoms */
    for( int j(0) ; j < nbAtom ; ++j ){
					
      /* Atom relevant vector's norms processing */
      // compute the equation (6) of our paper An Atomic Decomposition Scheme 
      data->processNormGradIGMAbsOut(j); // rotational independent
      // compute the equation (5) of our paper An Atomic Decomposition Scheme
      data->processNormGradIGMAbsIn(j); // rotational independent
					
      // Sum the delta gInterAtom for current atom j over the cube (equation (7) our our 
      // paper An Atomic Decomposition Scheme 
      // 10.1021/acs.jcim.9b01016) 
      results.updateAtom(j, *data/*, rh0*/); // rotational independent
					
    } // end over atoms

		
	
    /* DEPRECATED for CARDAN's method: Determinant was not inferior to 0 
  }else{ 
			
    //	if(++lol2>7000)std::cout << omp_get_thread_num() << " : "  << lol2 << " / " << std::scientific << determinant << " " << rho << std::endl;
				
    // Setting values to default 
    results.update(index, rho); 
				
  } // end of else of if determinant < 0 */

} // end of Node::process
void
Node::diagonalizeHessian()
{  // first, the eigV array 2x2 is used to initialize the matrix to be diagonalized
   eigV[0][0] = hess[0]; // 1_D array 'hess' is known in current Node
   eigV[0][1] = hess[1];
   eigV[0][2] = hess[2];
   eigV[1][0] = hess[1];
   eigV[1][1] = hess[3];
   eigV[1][2] = hess[4];
   eigV[2][0] = hess[2];
   eigV[2][1] = hess[4];
   eigV[2][2] = hess[5];

// make the matrix tridiagonal
   double d[3]; // diagonal elements after tridiagonalization
   double e[3]; // subdiagonal elements after tridiagonalization
   // hessian = the matrix to be diagonalized
   tred2(eigV, d, e);

// diagonalize
// eigV = eigenVectors = result
// d = eigenvalues in ascending order = result
// e = (subdiagonal elements, has been destroyed at the end)
// hessian = input and output = the eigenvectors associated with eigenvalues
   tql2(eigV, d, e);

  // get the eigenvalues lambdas
    lambdas[0] = d[0];
    lambdas[1] = d[1];
    lambdas[2] = d[2]; 

} // end of Node::getLambdaOfHessian


// routine to change the basis for the diffX, diffY and diffZ vectors for every atom
// (dx,dy,dz distance between current Nodes and atoms)
void 
Node::basisChange(double matrixHessianEigVec[3][3])
{

double vec1[3];
double vec2[3];

/* Looping through the atoms in the first FRAGMENT */
  for( int j(0) ; j < nbAtom ; ++j ){

// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// ==> basis change for diffX, diffY and diffZ
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    vec1[0] = diffX[j];     
    vec1[1] = diffY[j];    
    vec1[2] = diffZ[j];    

  //change the coord system from XYZ to local ED curvatures
    fortranMatMul33Matrix3Vector(matrixHessianEigVec,vec1,vec2);

    diffX[j] = vec2[0];
    diffY[j] = vec2[1];
    diffZ[j] = vec2[2];

   } // end of loop over atoms

} // end of method basisChange
#endif
