/* 
 *      Copyright (c) 2020 Robert Shaw
 *		This file is a part of Libecpint.
 *
 *      Permission is hereby granted, free of charge, to any person obtaining
 *      a copy of this software and associated documentation files (the
 *      "Software"), to deal in the Software without restriction, including
 *      without limitation the rights to use, copy, modify, merge, publish,
 *      distribute, sublicense, and/or sell copies of the Software, and to
 *      permit persons to whom the Software is furnished to do so, subject to
 *      the following conditions:
 *
 *      The above copyright notice and this permission notice shall be
 *      included in all copies or substantial portions of the Software.
 *
 *      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 *      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 *      MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 *      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 *      LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 *      OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 *      WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "gaussquad.hpp"
#include <cassert>
#include <cmath>
#include <cassert>
#include <iostream>

namespace libecpint {

	// Constructor
	GCQuadrature::GCQuadrature() {
		// Currently does nothing
	}


	GCQuadrature::GCQuadrature(const GCQuadrature &other) {
		maxN = other.maxN;
		M = other.M;
		I = other.I;
		t = other.t;
		x = other.x;
		w = other.w;
	}

	// Initialise the quadrature grid
	// As described in both Perez92 and Perez93
	void GCQuadrature::initGrid(const int points, const GCTYPE _t) {
		t = _t;
	
		// Initialise parameters for grid
		int p;
		if (t == ONEPOINT) { // Perez92 one point method
			// We need the number of points to be of the form
			// 2^p - 1 for some power p. 
			p = (int) floor(log(points + 1)/log(2));
			maxN = pow(2, p) - 1;
		} else if (t == TWOPOINT) { // Perez93 two point method
			// Here we need it instead to be of the form
			// 3 * 2^p - 1 for some p.
			p = (int) floor(log((points + 2)/3.0)/log(2));
			maxN = 3*pow(2, p) - 1;
		}
		M = (maxN-1)/2; // Midpoint
	
		// initialise arrays
		x.assign(maxN, 0.0);
		w.assign(maxN, 0.0);
	
		// At the midpoint, M, x[M] = 0 and w[M] = 1
		x[M] = 0.0; w[M] = 1.0;
		// The rest of the abscissae and weights are then given by:
		// z_i = i*Pi / (maxN + 1), s_i = sin(z_i), c_i = cos(z_i)
		// x_i = 1 + 2/(3*pi) * [ (3 + 2*s_i^2) * c_i * s_i - 3z_i]
		// (3(maxN + 1)/16) * w_i = s_i^4
		// We then note that s_(i+1) = c_1 s_i + s_1 c_i
		// and c_(i+1) = c_1 c_i - s_1 s_i
		// with z_(i+1) = z_i + z_1
		// Clearly s_(maxN + 1 - i) = s_i
		// and c_(maxN + 1 - i) = -c_i
		// Therefore x_(maxN + 1 - i) = -x_i
		// and w_(maxN + 1 - i) = w_i
		// Meaning that we only have to calculate half the weights and abscissae
		double z1 = M_PI / ((double)(maxN + 1));
		double c1 = cos(z1); double s1 = sin(z1);
		double zi, si, ci, zi1, si1, ci1; //z_i, s_i, c_i, z_(i+1), s_(i+1), c_(i+1)
		zi1 = z1; si1 = s1; ci1 = c1;
		double o23pi = 2.0 / (3.0 * M_PI); // Convenient
		double s2; //si * si
		for (int n = 0; n < M; n++) {
			// First update zi, si, ci
			zi = zi1;
			si = si1;
			ci = ci1;
			s2 = si * si;
		
			// Now determine the w and x values
			w[maxN - 1 - n] = w[n] = s2 * s2;
			x[n] = 1 + o23pi * ( (3.0 + 2.0 * s2) * ci * si - 3.0*zi );
			x[maxN - 1 - n] = x[n];
			x[n] = -x[n];
		
			// Then update zi1, si1, ci1
			zi1 = zi + z1;
			si1 = c1 * si + s1 * ci;
			ci1 = c1 * ci - s1 * si;
		}
	
		/*std::cout << maxN << " " << M << " " << start << " " << end << "\n";
		for (int q = 0; q < maxN; q++) std::cout << x[q] << " " << w[q] << "\n";*/
	}

	// Perform the GC integration on the function f
	std::pair<double, bool> GCQuadrature::integrate(
	    std::function<double(double, const double*, int)> &f, const double *params,
	    const double tolerance, const int start, const int end) const {
		bool converged = false; // 0 for converged, -1 for not converged

		double I = 0;
		if (t == ONEPOINT) {
			// Perez92 Case
			// Integration proceeds in the sequence T_1, T_3, T_7, ..., T_{maxN}
			// where T_m = (3(m+1)/16)I_m
			// by using the fact that T_{2m + 1} = T_{m} + sum_{k = 0}^m w_{2k+1}f(x_{2k+1})
			// The indices in terms of the maxN indices are given by 
			// 2k + 1 = (2k + 1) * M / 2^n = (2k + 1) * p
			// and checking convergence via whether
			// (T_{2m + 1} - 2T_m)^2 <= |T_{2m+1} - 4T_{(m-1)/2}| x tolerance
			double Tn, T2n1, Tn12; // T_n, T_{2n+1} and 4T_{(n-1)/2}
		
			// Initialise values, 
			// Single point integration would use midpoint, M
			Tn = w[M]*f(x[M], params, M);
			Tn12 = 2.0 * Tn;
		
			// Main loop
			int n = 1;
			double dT; // T_{2n+1} - 2T_n
			int ix; // Index needs to be calculated to know which points to use
			int p = (M+1) / 2; // M / 2^n 
			while (n < maxN && !converged) {
				// Compute T_{2n+1}
				T2n1 = Tn + sumTerms(f, params, n, start, end, p, 2);
			
				// Check convergence
				dT = T2n1 - 2.0*Tn;
				n = 2*n + 1;
				if (dT*dT <= fabs(T2n1 - Tn12)*tolerance) {
					converged = true;  
				} else {
					Tn12 = 4.0 * Tn; 
					Tn = T2n1;
					p /= 2; 
				}
			}
			// Finalise the integral
			I = 16.0 * T2n1 / (3.0*(n + 1.0));
		
		} else if (t == TWOPOINT) {
			// Perez93 case
			// We instead proceed along T_2, T_5, T_11, ..., T_{2m + 1} where maxN = 2m+1
			// but also compute T_1, T_3, ..., T_n, T_{2n+1} etc. as before,
			// where m + 1 = 3/2(n+1), so as to get better error control
			// To do this, we use that 
			// T_{2m+1} = T_m + T_n - T_{(n-1)/2} + sum_{i=0}^{(m-2)/3} [w_{6i+1}f(x_{6i+1}) + w_{6i+5}f(x_{6i+5})]
			// along with the same results as before. 
			// The algorithm proceeds by calculating one in the two-point sequence,
			// using an error of |I_{2m+1} - I_m|, then calculates one in the one-point sequence
			// and uses an error of |I_m - I_n|, to check convergence.
			double Tn, Tm, T2n1, T2m1, Tn12;
		
			// Initialise values
			Tn12 = 0.0; 
			Tn = w[M]*f(x[M], params, M);
			int M2 = (maxN - 2)/3; //Index of first point in twopoint sequence
			Tm = w[M2]*f(x[M2], params, M2) + w[maxN - M2 - 1]*f(x[maxN - M2 - 1], params, maxN - M2 - 1);
			int p = (M+1) / 2; // as before
			M2 = (M2 + 1)/2; 
			int ix; 
			int n = 1; int m = 2;
			double error;
		 
			while(m < maxN && !converged) {
				// Propagate the two-point sequence first 
				T2m1 = Tm + Tn - Tn12 + sumTerms(f, params, (2*m - 1)/3, start, end, M2, 3);
			
				// Check convergence
				error = 16.0 * fabs(0.5*T2m1 - Tm) / (3.0 * (m + 1)); 
				if (error > tolerance) {
					// Propagate the one-point sequence
					T2n1 = Tn + sumTerms(f, params, n, start, end, p, 2);
				
					// Check convergence again
					error = 16.0 * fabs(2.0*T2m1 - 3.0*T2n1) / (18.0 * (n+1) );
					m = 2 * m + 1;
					n = 2 * n + 1;
					if ( error < tolerance) {
						converged = true; 
					} else {
						Tn12 = Tn;
						Tn = T2n1;
						Tm = T2m1; 
						p /= 2;
						M2 /= 2; 
					}
				} else {
					m = 2 * m + 1;
					converged = true; 
				}
			}
			// Finalise the integral
			I = 16.0 * T2m1 / (3.0 * (m + 1.0));
		}
	
		return {I, converged};
	}

	// Worker function to do the additional sum terms when going from I_n to I_{2n+1}
	double GCQuadrature::sumTerms(
	    const std::function<double(double, const double*, int)> &f,
	    const double *p, const int limit, const int start, const int end,
	    const int shift, const int skip) const {
		assert(start >= 0 && start < maxN);
		assert(end >= 0 && end < maxN);
		assert(end >= start);

		double value = 0.0;
		int ix; 
		for (int i = 0; i <= limit; i+=2) {	
			ix = (skip*i + 1)*shift - 1;
			if (ix >= start)
				value += w[ix] * f(x[ix], p, ix);
		
			ix = maxN - ix - 1; 
			if (ix <= end)
				value += w[ix] * f(x[ix], p, ix);
		}
		return value;
	}

	// The GC integrations above are over the interval [-1, 1] and thus need to be transformed
	// to the interval[0, infty), or [rmin, rmax]. We do this by the logarithmic transformation from Krack98
	// or the linear mapping of Flores06, respectively.  
	void GCQuadrature::transformZeroInf() {
		double ln2 = log(2.0);
		double xt;
	
		for (int i = 0; i < maxN; i++) {
			xt = 1.0 - log(1.0-x[i])/ln2;
			w[i] = w[i]/(ln2 * (1.0 - x[i]));
			x[i] = xt;
		}
	}

	void GCQuadrature::transformRMinMax(const double z, const double p) {
		double osz = 1.0 / sqrt(z);
	
		// Determine interval
		double rmin = p - 7.0 * osz;
		rmin = rmin > 0 ? rmin : 0.0;
		double rmax = p + 9.0 * osz;
	
		// Find the relative and absolute midpoints 
		double rmid = 0.5*(rmax - rmin); // Midpoint of interval relative to rmin
		double amid = rmid + rmin; // Midpoint of interval
	
		// Transform weights and abscissae by linearly transforming
		// both are scaled by rmid, and the abscissae are translated by amid
		for (int i = 0; i < maxN; i++) {
			x[i] = rmid * x[i] + amid;
			w[i] *= rmid;
		}
	}
	
	void GCQuadrature::untransformRMinMax(const double z, const double p) {
		double osz = 1.0 / sqrt(z);
	
		// Determine interval
		double rmin = p - 7.0 * osz;
		rmin = rmin > 0 ? rmin : 0.0;
		double rmax = p + 9.0 * osz;
	
		// Find the relative and absolute midpoints 
		double rmid = 0.5*(rmax - rmin); // Midpoint of interval relative to rmin
		double amid = rmid + rmin; // Midpoint of interval
	
		// Transform weights and abscissae by linearly transforming
		// both are scaled by rmid, and the abscissae are translated by amid
		for (int i = 0; i < maxN; i++) {
			x[i] = (x[i] - amid) / rmid;
			w[i] /= rmid;
		}
	}

}
