#include <i3d/image3d.h>
#include <i3d/transform.h>
#include <i3d/morphology.h>

#include <stdlib.h> //for rnd generator seeding
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <sstream>
#include <fstream>

#include "params.h"
#include "graphics.h"
#include "TriangleMesh.h"
#include "rnd_generators.h"

//pressures:
#include <gsl/gsl_rng.h>
#include <gsl/gsl_randist.h>
#include <fftw3.h>
#ifndef __APPLE__
	#include <values.h>		//defines FLT_MAX and FLT_MIN on Linux
#else
	#include <float.h>		//defines FLT_MAX and FLT_MIN on Apple
#endif
#include <unistd.h>		//for getpid() in constructors
#include <fcntl.h>		//for creat() in constructors
#include <time.h>			//for timestamps in Scheduler::Run()
#include <i3d/toolbox.h> //pro GetNumberOfProcessors()

//create some shared objects...
ParamsClass params;
void ParamsSetup(void);
//
ActiveMesh mesh;
Vector3F sceneRes(8.f); //make isotropic resolution at 8 px/um

bool LoadNewMesh(const char* path,const int ID,const int fileNo);
int PnInitArray(const std::string& fileName);
int PnInitArray(i3d::Vector3d<size_t> const &size, const size_t frames,
                const std::string& fileName);
int PnGetMeNonrigidHints(const float stretch,const size_t frames,i3d::Image3d<float> &Hints);

int main(int argc,char **argv)
{
	if (argc == 1)
	{
		//I'm executed with no params, provide help
		std::cout << "ADD SOME HEADER INFORMATION HERE\n\n";
		std::cout << "params:  cell_surf_mesh  number_of_frames  param1  folder_for_pressure_maps\n\n";
		std::cout << "The program will load object file (cell body surface mesh -- the first\n"
		             "parameter), will read the number of frames (for which the pressure map should be\n"
		             "pregenerated), will read the map bumpiness (the smaller the more the surface\n"
		             "pressure will be varied) and will store a bunch of txt files with pressure maps\n"
						 "in the folder given with the last parameter.\n";
		std::cout << "TODO info about pnArray.cache\n";
		return(-1);
	}

	//init the situation...
	ParamsSetup();

	//read cell body
	std::cout << "reading cell body: " << argv[1] << "\n";
	int retval=mesh.ImportVTK(argv[1]);
	if (retval) 
	{
		std::cout << "some error reading cell body: " << retval << "\n";
		return(1);
	}

	std::cout << "vertices   #: " << mesh.Pos.size() << "\n";
	std::cout << "triangles  #: " << mesh.ID.size()/3 << "\n";
	std::cout << "normals    #: " << mesh.norm.size() << "\n";

	//to be removed after GM is OK
	mesh.ScaleMesh(Vector3F(180.f,180.f,250.f));

	//position the mesh somewhat inside the scene
	mesh.CenterMesh(1.5f);
	//translate...
	mesh.TranslateMesh(mesh.meshShift);

	//extract the number of demanded frames
	size_t frames=(unsigned)atol(argv[2]);

	// init the 'power noise' array so that cells can make use of it
	std::string fileName("pnArray.cache");
	std::ifstream FLE(fileName.c_str()); 
	
	if (FLE.is_open()) 
	{ 
		// file exists, use it
		FLE.close();
		PnInitArray(fileName);
	} 
	else 
	{
		// file is probably not existing, create it
		// according to the given setting
		PnInitArray(i3d::Vector3d<size_t>(
		   (size_t)(mesh.meshPlaygroundSize.x*sceneRes.x),
		   (size_t)(mesh.meshPlaygroundSize.y*sceneRes.y),
		   (size_t)(mesh.meshPlaygroundSize.z*sceneRes.z)),
		   frames+2,fileName);
	} 

	const float bumpiness=(float)atof(argv[3]);
	i3d::Image3d<float> hints;
	if (PnGetMeNonrigidHints(bumpiness,frames,hints))
	{
		std::cout << "\nThere was some issue using the power noise image.\n";
		std::cout << "Please, remove the pnArray.cache file and run the Pressurator again.\n";
		return(10);
	}

	//now, export the rows into a proper files:
	//The last parameter is a folder name that contain output files:
	//pressure_folder\coord.txt - file with point coordinates
	//pressure_folder\pressures_T%.3d.txt - files with history of pressure magnitudes
	char fn[100];

	//sweep over the list and report their coordinates
	sprintf(fn,"%s/coord.txt",argv[4]);
	std::ofstream TXT(fn);
	if (!TXT.is_open())
	{
		std::cout << "Couldn't open " << fn << " for writing.\n";
		return 1;
	}

	//we need to re-read the cell body mesh so that vertex positions are reported those
	//that one can find in the VTK file, i.e., without any scaling nor translation
	std::cout << "re-reading cell body: " << argv[1] << "\n";
	mesh.ImportVTK(argv[1]);

	for (size_t i=0; i < mesh.Pos.size(); ++i)
		TXT << mesh.Pos[i].x << " " << mesh.Pos[i].y << " " << mesh.Pos[i].z << "\n";
	TXT.close();

	//report deformation hints for all time points
	const float* pH=hints.GetFirstVoxelAddr();
	for (size_t t=0; t < hints.GetSizeY(); ++t)
	{
		sprintf(fn,"%s/pressures_T%03lu.txt",argv[4],t);
		std::ofstream TXT(fn);
		if (!TXT.is_open())
		{
			std::cout << "Couldn't open " << fn << " for writing.\n";
			return 1;
		}

		for (size_t i=0; i < mesh.Pos.size(); ++i, ++pH)
			TXT << *pH << "\n";
		TXT.close();
	}
}


void ParamsSetup(void)
{
	//set up the environment
   params.sceneOffset=Vector3d<float>(0.f);
   params.sceneSize=Vector3d<float>(27.5f,27.5f,22.0f); //for sample cell

	params.sceneCentre=params.sceneSize;
	params.sceneCentre/=2.0f;
	params.sceneCentre+=params.sceneOffset;

	params.sceneOuterBorder=Vector3d<float>(2.f);
	params.sceneBorderColour.r=0.7f;
	params.sceneBorderColour.g=0.7f;
	params.sceneBorderColour.b=0.0f;

	params.inputCellsFilename="cells/cell%d.txt";
	params.numberOfAgents=2;

	params.friendshipDuration=10.f;
	params.maxCellSpeed=0.20f;

	//adapted for cell cycle length of 24 hours
	params.cellCycleLength=14.5*60; //[min]

	params.currTime=0.0f; //all three are in units of minutes
	params.incrTime=1.0f;
	params.stopTime=50.0f;

	params.imgSizeX=500; //pixels
	params.imgSizeY=500;
	params.imgResX=1.0f; //pixels per micrometer
	params.imgResY=1.0f;

	params.imgOutlineFilename="Outline%05d.tif";
	params.imgPhantomFilename="Phantom%05d.tif";
	params.imgMaskFilename="Mask%05d.tif";
	params.imgFluoFilename="FluoImg%05d.tif";
	params.imgPhCFilename="PhCImg%05d.tif";
	params.tracksFilename="tracks.txt";


	//re-seed the standard rnd generator
	//(which is used for Perlin texture)
	unsigned int seed=(1 << 20) * (unsigned int)time(NULL) * (unsigned int)getpid();
	srand(seed);
}


//-----------------------------------------------------------------------------
// helper functions for the Scheduler::PnInitArray() functions

//threading helper stuff...
#ifdef _WIN32
	#include <windows.h>
	volatile HANDLE hMutex = CreateMutex(NULL,FALSE,NULL);
#else
	#include <pthread.h>
	pthread_mutex_t job_queue_mutexPN = PTHREAD_MUTEX_INITIALIZER;
#endif

void MUTEX_LOCK()
{
#ifdef _WIN32 
	WaitForSingleObject(hMutex,INFINITE);
#else
	pthread_mutex_lock(&job_queue_mutexPN);
#endif
}

void MUTEX_UNLOCK()
{
#ifdef _WIN32
	ReleaseMutex(hMutex);
#else
	pthread_mutex_unlock(&job_queue_mutexPN);
#endif
}

#define PN_DEBUG_REPORT(msg); { std::cout << "PN: " << msg << "\n"; }
#define PN_ERROR_REPORT(msg);	{ std::cout << "PN: " << msg << "\n"; return(1); }

float *pnArray=NULL;
size_t pnSizes[4];
size_t pnRealWidth;

//-----------------------------------------------------------------------------
int PnInitArray(i3d::Vector3d<size_t> const &size, const size_t frames,
                const std::string& fileName)
{
	// if required, init multi-threaded computing
	const size_t CPUs = i3d::GetNumberOfProcessors();
	PN_DEBUG_REPORT("Going to use multiprocessing, found " << CPUs << " CPUs");

	MUTEX_LOCK();
	fftwf_init_threads();
	fftwf_plan_with_nthreads((int)CPUs);
	MUTEX_UNLOCK();

	// initialize random generator
	gsl_rng *r = gsl_rng_alloc(gsl_rng_default);
	unsigned long s=(unsigned long)-1 * (unsigned long) time(NULL);
	PN_DEBUG_REPORT("randomness started with seed " << s);
	gsl_rng_set(r,s);

	const float zCorrection=1.0f;

	//helper constants
	const size_t size_xComp=size.x/2 +1; //x size of the complex image
	const size_t max_size = std::max(size_xComp,
				std::max(size.y, std::max(size.z, frames)));
	const float freqNormalization = max_size*max_size*max_size*max_size*max_size*max_size;
	#define gaussian_noise_sigma  5
	const size_t image_sz = 2 * size_xComp * size.y * size.z * frames;

	// allocate random Fourier image
	// first, free if not empty
	if (pnArray) fftwf_free((fftwf_complex*)pnArray);
	pnArray = (float*)fftwf_malloc(image_sz * sizeof(float));

	//enough memory?
	if (pnArray == NULL) {
		std::ostringstream err;
		err << "Not enough memory for power noise image of "
		    << image_sz * sizeof(float) << " Bytes";

		PN_ERROR_REPORT(err.str());
	}

	PN_DEBUG_REPORT("Starting to init the Fourier image of size "
			<< image_sz*sizeof(float)/(1<<20) << " MBytes");

	//working pointer
	float *buffer = pnArray;

	const float TemporalFreqAttenuation=0.66f;

	// initialize random Fourier image
	for (size_t t=0; t < frames; ++t) {
	 //note: the higher the K in the t*K+1 term, the slower the temporal changes are
	 //note: the K is now the TemporalFreqAttenuation
	 const float tFreq=((float)t*TemporalFreqAttenuation+1.0f)
	 						* ((float)t*TemporalFreqAttenuation+1.0f);

	 for (size_t z=0; z < size.z; ++z) {
	  const float ztFreq=((float)z*zCorrection+1.0f) * ((float)z*zCorrection+1.0f) +tFreq;

	  for (size_t y=0; y < size.y; ++y) {
	   const float yztFreq=((float)y+1.0f) * ((float)y+1.0f) +ztFreq;

	   for (size_t x=0; x < size_xComp; ++x) {
		//size of the frequency vector which is associated to the
		//coordinate [x,y,z,t] in this 4D Fourier image
		const float frequency = ((float)x+1.0f) * ((float)x+1.0f)  +yztFreq;

		//attenuating normalized amplitude (weight, in fact) ...
		const float amplitude = freqNormalization/(frequency*frequency*frequency);

		//... of some random complex pixel from the Fourier image
		//note: fftwf_complex = float[2]

		*buffer=amplitude*(float)gsl_ran_gaussian(r, gaussian_noise_sigma); //real
		++buffer;
		*buffer=amplitude*(float)gsl_ran_gaussian(r, gaussian_noise_sigma); //imaginary
		++buffer;
	   }
	  }
	 }
	}

	// close the random generator
	gsl_rng_free(r);

	PN_DEBUG_REPORT("Starting inverse Fourier transform");

	//now, convert the prepared Fourier image into a spatial 'power noise' image
	MUTEX_LOCK();
	const int dims[4] = { (int)frames, (int)size.z, (int)size.y, (int)size.x };
	fftwf_plan plan = fftwf_plan_dft_c2r(4, dims, (fftwf_complex*)pnArray, pnArray, FFTW_ESTIMATE);
	MUTEX_UNLOCK();

	fftwf_execute(plan);
	fftwf_destroy_plan(plan);
	fftwf_cleanup();
	fftwf_cleanup_threads();

	PN_DEBUG_REPORT("Starting to normalize the power noise image");

	// normalize the output
	float fmin = FLT_MAX;
	float fmax = FLT_MIN;
	double fmean=0;

	for (size_t t = 0; t < image_sz; ++t) {
		fmin = std::min(fmin, *(pnArray+t));
		fmax = std::max(fmax, *(pnArray+t));
		fmean+= *(pnArray+t);
	}
	fmean/=(double)image_sz;

	//divide the span by 2, to allow it to stretch over [-1:+1]
	//
	//we have observed, with PnPrintArrayStats(), that the
	//'power noise' distribution resembles Gaussian with 
	//std. variation of 0.25 (and mean zero)
	const float MaxMinSpan=(fmax-fmin)/2.f;
	for (size_t t = 0; t < image_sz; ++t)
	{
		*(pnArray+t) -= fmean;
		*(pnArray+t) /= MaxMinSpan;
	}
	//note: values now range from [-1.0 : 1.0]
	//note: mostly occuring values range from [-0.5 : 0.5], a 4-sigma interval

	//save metadata
	pnRealWidth=size.x;
	pnSizes[0]= 2 * size_xComp;
	pnSizes[1]=size.y;
	pnSizes[2]=size.z;
	pnSizes[3]=frames;

	PN_DEBUG_REPORT("Created: RealWidth=" << pnRealWidth << ", pnSizes[]="
			<< pnSizes[0] << "," << pnSizes[1] << ","
			<< pnSizes[2] << "," << pnSizes[3]);

	//if (fileName) {
		PN_DEBUG_REPORT("Starting to save the cache file " << fileName);

		std::ofstream FLE;
		FLE.open(fileName.c_str(),std::ios_base::binary);
		if (!FLE.is_open()) {
			std::ostringstream err;
			err << "Can't open file " << fileName << " for writing.";
			PN_ERROR_REPORT(err.str());
		}

		/* text metadata */
		FLE << "x,y,z,t: " << pnSizes[0] << " " << pnSizes[1] << " "
		    << pnSizes[2] << " " << pnSizes[3] << "\n";
		FLE << "RealWidth: " << pnRealWidth << "\n";

		/* binary dara */
		FLE.write((const char*)pnArray,(signed)(image_sz*sizeof(float)));

		//everything OK?
		if (!FLE.good()) {
			std::ostringstream err;
			err << "Error writing data to file " << fileName << ".";
			PN_ERROR_REPORT(err.str());
		}

		FLE.close();
	//}

	PN_DEBUG_REPORT("Finished");
	return(0);
}


//-----------------------------------------------------------------------------
int PnInitArray(const std::string& fileName)
{
	PN_DEBUG_REPORT("Starting to load from cache file " << fileName);

	std::ifstream FLE;
	FLE.open(fileName.c_str(),std::ios_base::binary);
	if (!FLE.is_open()) {
		std::ostringstream err;
		err << "Can't open file " << fileName << " for reading.";
		PN_ERROR_REPORT(err.str());
	}

	/* text version */
	char skipChar;
	for (int q=0; q < 8; ++q) FLE >> skipChar;
	FLE >> pnSizes[0] >> pnSizes[1] >> pnSizes[2] >> pnSizes[3];
	for (int q=0; q < 10; ++q) FLE >> skipChar;
	FLE >> pnRealWidth;

	PN_DEBUG_REPORT("Discovered: RealWidth=" << pnRealWidth << ", pnSizes[]="
			<< pnSizes[0] << "," << pnSizes[1] << ","
			<< pnSizes[2] << "," << pnSizes[3]);

	//helper metainformation
	const size_t image_sz = pnSizes[0] * pnSizes[1] * pnSizes[2] * pnSizes[3];

	// allocate 'power noise' image
	// first, free if not empty
	if (pnArray) fftwf_free((fftwf_complex*)pnArray);
	pnArray = (float*)fftwf_malloc(image_sz * sizeof(float));

	//enough memory?
	if (pnArray == NULL) {
		std::ostringstream err;
		err << "Not enough memory for power noise image of "
		    << image_sz * sizeof(float) << " Bytes";

		PN_ERROR_REPORT(err.str());
	}

	PN_DEBUG_REPORT("Reading power noise image of size "
			<< image_sz*sizeof(float)/(1<<20) << " MBytes");

	//start reading right from the beginning of the third line
	//(for some reason it had to reinitialized like this...)
	FLE.seekg(0,std::ios_base::beg);
	FLE.getline((char*)pnArray,200);
	FLE.getline((char*)pnArray,200);
	FLE.read((char*)pnArray,(signed)(image_sz*sizeof(float)));
	
	//everything OK?
	if (!FLE.good()) {
		std::ostringstream err;
		err << "Error reading data from file " << fileName << ".";
		PN_ERROR_REPORT(err.str());
	}

	FLE.close();

	PN_DEBUG_REPORT("Finished");
	//the pnArray will be destroyed in the Scheduler::DESTRUCTOR
	return(0);
}

//-----------------------------------------------------------------------------
int PnGetMeNonrigidHints(const float stretch,const size_t frames,i3d::Image3d<float> &Hints)
{
	//consistency checks...
	if (pnArray == NULL)
		{ PN_ERROR_REPORT("'power noise' image (pnArray) is not ready"); }
	if (mesh.Pos.size() == 0)
		{ PN_ERROR_REPORT("List of vertices is given empty"); }
	if (pnSizes[3] == 0)
		{ PN_ERROR_REPORT("'power noise' image (pnArray) is crippled, it is ready for zero frames"); }

	if (pnSizes[3] < frames+1)
		{ PN_ERROR_REPORT("'power noise' image (pnArray) is too short to create " << frames << " frames"); }

	//find the (rectangular coordinate-system axis-aligned) bounding box
	//in microns...
	float xFrom=mesh.Pos[0].x;
	float yFrom=mesh.Pos[0].y;
	float zFrom=mesh.Pos[0].z;
	float xTo=  mesh.Pos[0].x;
	float yTo=  mesh.Pos[0].y;
	float zTo=  mesh.Pos[0].z;

	//sweep over the list and seek extremal coordinates
	for (size_t i=1; i < mesh.Pos.size(); ++i) {
		//tested position
		const float x=mesh.Pos[i].x;
		const float y=mesh.Pos[i].y;
		const float z=mesh.Pos[i].z;

		if (x < xFrom) xFrom=x;
		if (y < yFrom) yFrom=y;
		if (z < zFrom) zFrom=z;
		if (x > xTo) xTo=x;
		if (y > yTo) yTo=y;
		if (z > zTo) zTo=z;
	}

	PN_DEBUG_REPORT("points span x=" << xFrom << " to " << xTo << "; y="
	       << yFrom << " to " << yTo << "; z=" << zFrom << " to " << zTo
	       << " in microns");

	//minimum required size of the pnArray in pixels
	const size_t xSize=(size_t)ceilf( (xTo-xFrom) * sceneRes.x );
	const size_t ySize=(size_t)ceilf( (yTo-yFrom) * sceneRes.y );
	const size_t zSize=(size_t)ceilf( (zTo-zFrom) * sceneRes.z );
	PN_DEBUG_REPORT("span in pixels is " << xSize << "; " << ySize << "; " << zSize);

	if ((xSize > pnRealWidth) || (ySize > pnSizes[1]) || (zSize > pnSizes[2]))
		{ PN_ERROR_REPORT("given cell is larger than 'power noise' image (pnArray)"); }

	//now, determine some random integer pixel offset
	const size_t xOffs=(size_t)floorf(GetRandomUniform(0,pnRealWidth-xSize));
	const size_t yOffs=(size_t)floorf(GetRandomUniform(0,pnSizes[1]-ySize));
	const size_t zOffs=(size_t)floorf(GetRandomUniform(0,pnSizes[2]-zSize));
	PN_DEBUG_REPORT("since pnArray has size " << pnRealWidth << "," << pnSizes[1]
		     << "," << pnSizes[2] << ", will use offset " << xOffs << ","
	     	     << yOffs << "," << zOffs);

	//init output image
	Hints.MakeRoom(mesh.Pos.size(),frames,1);  //Dima required the defor. map even for the last frame, otherwise frames-1 should be used
	Hints.SetOffset(i3d::Offset(0.f,0.f,0.f));
	Hints.SetResolution(i3d::Resolution(1.0f,1.0f,1.0f));

	float *pH=Hints.GetFirstVoxelAddr();

	// helper constants
	// note: the pnArray 'power noise' image may be padded
	const size_t buffer_line_length = pnSizes[0];
	const size_t slice_sz = pnSizes[1] * buffer_line_length;
	const size_t frame_sz = pnSizes[2] * slice_sz;

	//to access pixel at [x,y,z,t], one needs to look at
	// *(pnArray + t*frame_sz + z*slice_sz + y*buffer_line_length + x)
	// it has a type 'float'

	//make a copy of values we plan to use for the Hints image
	//and compute their stats at the same time
	for (size_t t=1; t <= frames; ++t) {
	  float *pA=pnArray + t*frame_sz;

	  for (size_t i=0; i < mesh.Pos.size(); ++i, ++pH) {
		//convert absolute micron size to pixels within the pnArray
		const size_t x=(size_t)roundf((mesh.Pos[i].x-xFrom)*sceneRes.x) + xOffs;
		const size_t y=(size_t)roundf((mesh.Pos[i].y-yFrom)*sceneRes.y) + yOffs;
		const size_t z=(size_t)roundf((mesh.Pos[i].z-zFrom)*sceneRes.z) + zOffs;
		*pH= *(pA + z*slice_sz + y*buffer_line_length + x);
		*pH-=*(pA + z*slice_sz + y*buffer_line_length + x -frame_sz);
	  }
	}

	//subtract means and stretch appropriately, row by row
	pH=Hints.GetFirstVoxelAddr();
	for (size_t t=0; t < frames; ++t) {
		//first, observe current situation in the row
		double mean=0.;
		float minPN=10000.f;
		float maxPN=-10000.f;
		for (size_t i=0; i < mesh.Pos.size(); ++i) {
			mean+=*(pH+i);
			if (*(pH+i) < minPN) minPN=*(pH+i);
			if (*(pH+i) > maxPN) maxPN=*(pH+i);
		}

		//plan changes:
		//mean to subtract
		const float fmean=static_cast<float>(mean/(double)mesh.Pos.size());

		//stretch factor so that maximum detected shift of any boundary
		//point is cellNonRigidDeformation microns, new extremas:
		minPN=fabsf(minPN-fmean);
		maxPN=fabsf(maxPN-fmean);
		if (minPN > maxPN) maxPN=minPN;
		//note: maxPN now holds max(minPN,maxPN)
		//const float stretch=(float)configIni["cell"]["cell nonrigid deformation"]/maxPN;
		//PN_DEBUG_REPORT("row " << t-1 << "\t mean is " << fmean << ", stretch is " << stretch);

		//commit changes
		for (size_t i=0; i < mesh.Pos.size(); ++i, ++pH) *pH=(*pH-fmean)*stretch/maxPN;
	}

	return(0);
}


bool LoadNewMesh(const char* path,const int ID,const int fileNo)
{
	//this function is here only because the graphics.cpp module references it
	//but graphics is not used in this module at all...

	//this is here to get rid of warnings...
	int a=(int)path[0];
	a+=fileNo;
	a+=ID;
	return(true);
}
