//This file is part of necsim project which is released under MIT license.
//See file **LICENSE.txt** or visit https://opensource.org/licenses/MIT) for full license details.

/**
 * @author James Rosindell
 * @file NRrand.h
 * @brief Contains a generic random number generator.
 * Provided by James Rosindell (j.rosindell@imperial.ac.uk) with moderate modifications by
 * Samuel Thompson (thomsonsed@gmail.com).
 * 
 * The definitions for the constants defined here should not be altered.
 * @copyright <a href="https://opensource.org/licenses/MIT"> MIT Licence.</a>
 */
#ifndef FATTAIL_H
#define FATTAIL_H

#include <cstdio>
#include <string>
#include <iomanip>

#define _USE_MATH_DEFINES

#include <cmath>
#include <algorithm>
#include <vector>
#include <iostream>
#include <fstream>
#include <climits>
#include "Logger.h"

using namespace std;
/**
 * @brief These variables contain the special numbers for random number generation.
 */
const long IM1 = 2147483563;
const long IM2 = 2147483399;
const double AM = (1.0/IM1);
const long IMM1 = (IM1-1);
const long IA1 = 40014;
const long IA2 = 40692;
const long IQ1 = 53668;
const long IQ2 = 5277;
const long IR1 = 12211;
const long IR2 = 3791;
const long NTAB = 32;
const double NDIV = (1+IMM1/NTAB);
const double EPS = 1.2e-8;
const double RNMX(1.0-EPS);

/**
 * @brief Contains the functions for random number generation.
 */
class NRrand {

private:
	long idum;
	int j;
	long k;
	long idum2;
	long iy;
	long iv[NTAB];
	double temp;
	bool seeded;

	double lastresult;
	bool normflag;
	// for the L value of the dispersal kernel (the width - does not affect the shape).
	double tau;
	// for the sigma value of the dispersal kernel (the variance of a normal distribution).
	double sigma;

	typedef double (NRrand::*fptr)(); // once setup will contain the dispersal function to use for this simulation.
	fptr dispersalFunction;

	// once setup will contain the dispersal function for the minimum dispersal distance.
	typedef double (NRrand::*fptr2)(const double& min_distance);

	fptr2 dispersalFunctionMinDistance;

	// the probability that dispersal comes from the uniform distribution. This is only relevant for uniform dispersals.
	double m_prob;
	// the cutoff for the uniform dispersal function i.e. the maximum value to be drawn from the uniform distribution.
	double cutoff;
public:

	/**
	 * @brief Standard constructor.
	 */
	NRrand()
			:idum(0), j(0), k(0), idum2(0), iy(0), iv{0}, temp{0.0}, seeded{false}, lastresult{0.0}, normflag{true},
			 tau(0.0), sigma(0.0), dispersalFunction(nullptr), dispersalFunctionMinDistance(nullptr), m_prob(0.0),
			 cutoff(0.0)
	{

	}

	/**
	 * @brief Sets the seed to the given input.
	 * Is only seeded if the seed hasn't already been provided.
	 * @param seed the input seed.
	 */
	void setSeed(long seed)
	{
		if (!seeded) {
			idum2 = 123456789;
			iy = 0;
			idum = abs(seed);
			if (idum<1) idum = 1;
			//Be sure to prevent idum = 0.
			idum2 = idum;
			for (j = NTAB+7; j>=0; j--) {
				//Load the shuffle table (after 8 warm-ups).
				k = (idum)/IQ1;
				idum = IA1*(idum-k*IQ1)-k*IR1;
				if (idum<0) idum += IM1;
				if (j<NTAB) iv[j] = idum;
			}
			iy = iv[0];
			seeded = true;
		}
		else {
			throw runtime_error("Trying to set the seed again: this can only be set once.");
		}
	}

	/**
	 * @brief Clears the seed, if it has already been set.
	 * Keeps other simulation parameters, such as sigma and tau.
	 */
	void wipeSeed()
	{
		seeded = false;
	}

	/**
	 * @brief The random number generator.
	 * Uses Schrage's method and a shuffle table to generate the output.
	 * @return the random number (a double between 0 and 1).
	 */
	double d01()
	{
		k = (idum)/IQ1;
		//Start here when not initializing.
		idum = IA1*(idum-k*IQ1)-k*IR1;
		//Compute idum=(IA1*idum) % IM1 without overflows by Schrage's method. 
		if (idum<0) idum += IM1;
		k = idum2/IQ2;
		idum2 = IA2*(idum2-k*IQ2)-k*IR2;
		//Compute idum2=(IA2*idum) % IM2 likewise.
		if (idum2<0) idum2 += IM2;
		j = iy/NDIV;
		//Will be in the range 0..NTAB-1.
		iy = iv[j]-idum2;
		//Here idum is shuffled, idum and idum2 are combined to generate output. 
		iv[j] = idum;
		if (iy<1) iy += IMM1;
		if ((temp = AM*iy)>RNMX) {
			return RNMX; //Because users don't expect endpoint values.
		}
		return temp;

	}

	/**
	 * @brief Generates a random number uniformly from 0 to the maximum value provided.
	 * @param max the maximum number.
	 * @return an integer of the produced random number.
	 */
	unsigned long i0(unsigned long max)
	{
		return (unsigned long) (d01()*(max+1));
	}

	/**
	 * @brief Generates a normally distributed number
	 * Uses the standard normal distribution from a Box-Muller transform.
	 * @return the random number from a normal distribution.
	 */
	double norm()
	{
		if (normflag) {
			double r2 = 2;
			double xx = 0;
			double yy = 0;
			while (r2>1) {
				xx = 2.0*d01()-1.0;
				yy = 2.0*d01()-1.0;
				r2 = (xx*xx)+(yy*yy);
			}
			double fac = sqrt(-2.0*log(r2)/r2);
			lastresult = xx*fac;
			double result = yy*fac;
			normflag = false;
			return sigma*result;
		}
		else {
			normflag = true;
			return sigma*lastresult;
		}
	}

	/**
	 * @brief Returns a random distance from a 2 dimensional normal distribution, also called the rayleigh distribution.
	 * @return dispersal distance of a rayleigh distribution
	 */
	double rayleigh()
	{
		return sigma*pow(-2*log(d01()), 0.5);
	}

	/**
	 * @brief Generates a random distance from a rayleigh distribution, given that the distance is more than some
	 * minimum.
	 * @param dist the minimum distance to generate
	 * @return a random distance greater than the minimum provided
	 */
	double rayleighMinDist(const double& dist)
	{
		double min_prob = rayleighCDF(dist);
		double rand_prob = min_prob+(1-min_prob)*d01();
		double out = sigma*pow(-2*log(rand_prob), 0.5);
		if (out<dist) {
			// This probably means that the rayleigh distribution has a less-than-machine-precision probability of
			// producing this distance.
			// Therefore, we just return the distance
			return dist;
		}
		return out;
	}

	/**
	 * @brief Gets the cumulative probability of a distance from the rayleigh distribution
	 * @param dist the distance to obtain the probability of
	 * @return the probability of producing the given distance
	 */
	double rayleighCDF(const double& dist)
	{
		return 1-exp(-pow(dist, 2.0)/(2.0*pow(sigma, 2.0)));
	}

	/**
	 * @brief Sets the dispersal parameters, avoiding requirement to provide these numbers each function call.
	 * This is only relevant for fat-tailed dispersal calls.
	 * @param sigmain the fatness of the fat-tailed dispersal kernel.
	 * @param tauin the width of the fat-tailed dispersal kernel.
	 */
	void setDispersalParams(const double sigmain, const double tauin)
	{
		sigma = sigmain;
		tau = tauin; // used to invert the sign here, doesn't any more.
	}

	/**
	 * @brief Call from the fat-tailed dispersal kernel with the provided sigma.
	 * @deprecated This is the original version used in J Rosindell's codebase, and has been altered for
	 * a version which approximates the gaussian distribution at extreme limits.
	 * @param z the desired sigma.
	 * @return a random number drawn from the fat-tailed dispersal kernel.
	 */
	double fattail(double z)
	{
		double result;
		result = pow((pow(d01(), (1.0/(1.0-z)))-1.0), 0.5);
		return result;
	}

	/**
	 * @brief Gets the cumulative probability density of travelling the distance
	 * @param distance the distance to obtain the cumulative probability for
	 * @return the probability of dispersing less than or equal to distance
	 */
	double fattailCDF(const double& distance)
	{
		return (1.0/(2.0*M_PI*sigma*sigma))*
				pow(1+(distance*distance/(tau*sigma*sigma)), -(tau+2.0)/2.0);
	}

	/**
	 * @brief Gets a fat-tailed random distance greater than some minimum
	 * @param min_distance the minimum distance to return
	 * @return a fat-tailed distance greater than the minimum
	 */
	double fattailMinDistance(const double& min_distance)
	{
		double prob = fattailCDF(min_distance);
		double random_number = prob+d01()*(1-prob);
		return (sigma*pow((tau*(pow(random_number, -2.0/tau))-1.0), 0.5));
	}

	// this new version corrects the 1.0 to 2.0 and doesn't require the values to be passed every time.
	/**
	 * @brief Call from fat-tailed dispersal kernel.
	 * This function requires setDispersalParams() has already been called.
	 * @deprecated deprecated, kept for testing purposes only
	 * @return a random number drawn from the fat-tailed dispersal kernel.
	 */
	double fattail()
	{
		double result;
		// old function version (kept for reference)
//		result = (tau * pow((pow(d01(),(2.0/(2.0-sigma)))-1.0),0.5));
		result = (sigma*pow((tau*(pow(d01(), -2.0/tau))-1.0), 0.5));
		return result;
	}

	/**
	 * @brief Old version of the function call reparameterised for different nu and sigma.
	 * @deprecated Kept only for testing purposes.
	 * @return a random number drawn from the fat-tailed dispersal kernel.
	 */
	double fattail_old()
	{
		double result;
		result = (sigma*pow((pow(d01(), (2.0/(2.0+tau)))-1.0), 0.5));
		return result;
	}

	/**
	 * @brief Generates a direction in radians.
	 * @return the direction in radians
	 */
	double direction()
	{
		return (d01()*2*M_PI);
	}

	/**
	 * @brief For a given event probability, returns the probability that the event has occured.
	 * @param event_probability the event probability.
	 * @return whether or not the event has occured.
	 */
	bool event(double event_probability)
	{
		if (event_probability<0.000001) {
			if (d01()<=0.000001) {
				return (event(event_probability*1000000.0));
			}
			return false;
		}
		if (event_probability>0.999999) {
			return (!(event(1.0-event_probability)));
		}
		return (d01()<=event_probability);

	}

	/**
	 * @brief Normal distribution, with percentage chance to choose a uniform distribution instead. 
	 * @note This function will not produce the same output as norm() for the same parameters, even with a
	 * zero chance of picking from the uniform distribution (due to random number draws).
	 * @return normally (or uniformly) distributed number
	 */
	double normUniform()
	{
		// Check if the dispersal event comes from the uniform distribution
		if (d01()<m_prob) {
			// Then it does come from the uniform distribution
			return uniform();
		}
		return rayleigh();
	}

	/**
	 * @brief Generates a random distance from a norm-uniform distribution, given that the distance is more than some
	 * minimum.
	 * @param dist the minimum distance to generate
	 * @return a random distance greater than the minimum provided
	 */
	double normUniformMinDistance(const double& min_distance)
	{
		if (d01()<m_prob) {
			// Then it does come from the uniform distribution
			return uniformMinDistance(min_distance);
		}
		return rayleighMinDist(min_distance);
	}

	/**
	 * @brief Draws a random number from a uniform distribution between 0 and cutoff
	 * @return a random number in (0, cutoff)
	 */
	double uniform()
	{
		return d01()*cutoff;
	}

	/**
	 * @brief Generates a random distance from a uniform distribution, given that the distance is more than some
	 * minimum.
	 * @param dist the minimum distance to generate
	 * @return a random distance greater than the minimum provided
	 */
	double uniformMinDistance(const double& min_distance)
	{
		if (min_distance>cutoff) {
			// Note this may introduce problems for studies of extremely isolated islands
			// I've left this in to make it much easier to deal with scenarios where the
			// disappearing habitat pixel is further from the nearest habitat pixel than
			// the maximum dispersal distance
			return min_distance;
		}
		return min_distance+d01()*(cutoff-min_distance);
	}

	/**
	 * @brief Two uniform distributions, the first between 0 and 0.1*cutoff, and the second between 0.9*cutoff and
	 * cutoff. Selects from both distributions equally.
	 * @note The mean for this function should be identical to a uniform distribution between 0 and cutoff.
	 * @return uniformly distributed number
	 */
	double uniformUniform()
	{
		if (d01()<0.5) {
			// Then value comes from the first uniform distribution
			return (uniform()*0.1);
		}
		// Then the value comes from the second uniform distribution
		return 0.9*cutoff+(uniform()*0.1);
	}

	/**
	 * @brief Generates a random distance from a uniform-uniform distribution, given that the distance is more than some
	 * minimum.
	 * @param dist the minimum distance to generate
	 * @return a random distance greater than the minimum provided
	 */
	double uniformUniformMinDistance(const double& min_distance)
	{
		if (d01()<0.5) {
			// Then value comes from the first uniform distribution
			if (min_distance>cutoff*0.1) {
				return uniformMinDistance(min_distance*10)*0.1;
			}
		}
		// Then the value comes from the second uniform distribution
		return uniformMinDistance(max(min_distance, 0.9*cutoff));
	}

	/**
	 * @brief Sets the dispersal method by creating the link between dispersalFunction() and the correct
	 * dispersal character
	 * @param dispersal_method string containing the dispersal type. Can be one of [normal, fat-tail, norm-uniform]
	 * @param m_probin the probability of drawing from the uniform distribution. Only relevant for uniform dispersals.
	 * @param cutoffin the maximum value to be drawn from the uniform dispersal. Only relevant for uniform dispersals.
	 */
	void setDispersalMethod(const string& dispersal_method, const double& m_probin, const double& cutoffin)
	{
		if (dispersal_method=="normal") {
			dispersalFunction = &NRrand::rayleigh;
			dispersalFunctionMinDistance = &NRrand::rayleighMinDist;
			if (sigma<0) {
				throw invalid_argument("Cannot have negative sigma with normal dispersal");
			}
		}
		else if (dispersal_method=="fat-tail" || dispersal_method=="fat-tailed") {
			dispersalFunction = &NRrand::fattail;
			dispersalFunctionMinDistance = &NRrand::fattailMinDistance;
			if (tau<0 || sigma<0) {
				throw invalid_argument("Cannot have negative sigma or tau with fat-tailed dispersal");
			}
		}
		else if (dispersal_method=="norm-uniform") {
			dispersalFunction = &NRrand::normUniform;
			dispersalFunctionMinDistance = &NRrand::normUniformMinDistance;
			if (sigma<0) {
				throw invalid_argument("Cannot have negative sigma with normal dispersal");
			}
		}
		else if (dispersal_method=="uniform-uniform") {
			// This is just here for testing purposes
			dispersalFunction = &NRrand::uniformUniform;
			dispersalFunctionMinDistance = &NRrand::uniformUniformMinDistance;
		}
			// Also provided the old version of the fat-tailed dispersal kernel
		else if (dispersal_method=="fat-tail-old") {
			dispersalFunction = &NRrand::fattail_old;
			dispersalFunctionMinDistance = &NRrand::fattailMinDistance;

			if (tau>-2 || sigma<0) {
				throw invalid_argument(
						"Cannot have sigma < 0 or tau > -2 with fat-tailed dispersal (old implementation).");
			}
		}
		else {
			throw runtime_error("Dispersal method not detected. Check implementation exists");
		}
		m_prob = m_probin;
		cutoff = cutoffin;
	}

	/**
	 * @brief Runs the dispersal with the allocated dispersal function.
	 *
	 * @note This function will never return a value larger than the size of LONG_MAX to avoid issues of converting
	 * doubles to integers. For dispersal distance within coalescence simulations, this is seemed a reasonable
	 * assumption, but may cause issues if code is re-used in later projects.
	 *
	 * @return distance the dispersal distance
	 */
	double dispersal()
	{
		return min(double(LONG_MAX), (this->*dispersalFunction)());
	}

	/**
	 * @brief Get a dispersal distance with some minimum
	 * @param min_distance the minimum distance to disperse
	 * @return the random dispersal distance greater than or equal to the minimum
	 */
	double dispersalMinDistance(const double& min_distance)
	{
		return min(double(LONG_MAX), (this->*dispersalFunctionMinDistance)(min_distance));
	}

	/**
	 * @brief Sample from a logarithmic distribution
	 *
	 * Uses the LK sampling method for generating random numbers from a logarithmic distribution, as described by
	 * Kemp (1981).
	 *
	 * @param alpha alpha parameter for the logarithmic distribution
	 * @return the randomly generated logarithmic number
	 */
	unsigned long randomLogarithmic(long double alpha)
	{
		double u_2 = d01();
		if (u_2>alpha) {
			return 1;
		}
		long double h = log(1-alpha);
		long double q = 1-exp(d01()*h);
		if (u_2<(q*q)) {
			return static_cast<unsigned long>(floor(1+log(u_2)/log(q)));
		}
		else if (u_2>q) {
			return 1;
		}
		else {
			return 2;
		}

	}

	/**
	 * @brief Outputs the NRrand object to the output stream.
	 * Used for saving the object to file.
	 * @param os the output stream.
	 * @param r the NRrand object to output.
	 * @return the output stream.
	 */
	friend ostream& operator<<(ostream& os, const NRrand& r)
	{
		//os << m.num_rows<<" , "<<m.num_cols<<" , "<<endl;
		os << setprecision(64);
		os << r.idum << ",";
		os << r.j << ",";
		os << r.k << ",";
		os << r.idum2 << ",";
		os << r.iy << ",";
		for (long i : r.iv) {
			os << i << ",";
		}
		os << r.temp << ",";
		os << r.seeded << ",";
		os << r.lastresult << ",";
		os << r.normflag << "," << r.tau << "," << r.sigma << "," << r.m_prob << "," << r.cutoff;
		return os;
	}

	/**
	 * @brief Inputs the NRrand object from the input stream.
	 * Used for reading the NRrand object from a file.
	 * @param is the input stream.
	 * @param r the NRrand object to input to.
	 * @return the input stream.
	 */
	friend istream& operator>>(istream& is, NRrand& r)
	{
//		os << "starting NR read" << endl;
		char delim;
		//double temp1,temp2;
		//is << m.num_rows<<" , "<<m.num_cols<<" , "<<endl;
		is >> r.idum;
//		os << r.idum << endl;
//		string tmp;
//		is >> delim >> tmp;
//		os << tmp << endl;
//		os << delim;
		is >> delim;
		is >> r.j;
		is >> delim;
		is >> r.k;
		is >> delim;
		is >> r.idum2;
		is >> delim;
		is >> r.iy;
		is >> delim;
		for (long& i : r.iv) {
			is >> i;
			is >> delim;
		}
		is >> r.temp;
		is >> delim;
		is >> r.seeded;
		is >> delim;
		is >> r.lastresult;
		is >> delim;
		is >> r.normflag;
		is >> delim >> r.tau >> delim >> r.sigma >> delim >> r.m_prob >> delim >> r.cutoff;
		return is;
	}
};

#endif