#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 <fstream>

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

//create some shared objects...
ParamsClass params;
void ParamsSetup(void);
//
ActiveMesh mesh;

//for filopodia tip tracking:
// GM addresses filopodia branches with pairs: filoTreeID + branchID;
// we linearize them with tipIndex() (e.g., as filoTreeID*100+branchID);
// this is the first index (primary key) in tipOffsets;
// the second index (secondary key) is time point and
// the value is actually a pixel offset (encoding a tip position)
//
//time-course (vector index) of pixel offsets (vector value)
class timeVector: public std::vector<int>
{
  public:
	void Init(const size_t length=200)
	{ this->insert(this->end(),length,-1); }
};
//
//this one holds the entire time-lapse of pixel offsets of all filopodia tips
//use tipIndex() below to address filopodia branches
std::map<int,timeVector> tipOffsets;
//
//this one, similar to the above, holds the entire time-lapse of tip coordinates
class timeVectorC: public std::vector<Vector3F>
{
  public:
	void Init(const size_t length=200)
	{ this->insert(this->end(),length,Vector3F(-1.f)); }
};
std::map<int,timeVectorC> tipCoords;
//
//use exclusively this one to address filopodia branches in tipOffsets above
int tipIndex(const int filoID,const int branchID)
{ return (filoID*100 +branchID); }

//save cell centres
std::vector<Vector3F> centreCoords;
std::vector<int> centreOffsets;

bool LoadNewMesh(const char* path,const int ID,const int fileNo,const bool keepTrajectories=false);
int  RenderMesh(const char* path,const int ID,int fileNo);

int main(int argc,char **argv)
{
	if (argc != 5 && argc != 6 && argc != 7)
	{
		//I'm executed with no params, provide help
		std::cout << "ADD SOME HEADER INFORMATION HERE\n\n";
		std::cout << "params:  config_file  path_to_VTKs path_to_output  dataset_ID  [time_point] [no_of_time_points]\n\n";
		std::cout << "The program will try to load object files (cell body volumetric mesh and\n"
                   "filopodia skeleton definition) iteratively (starting from 0th time point)\n"
                   "until it fails loading cell body file. Filopodia files for the current time\n"
                   "point are tried from the index 1. If the optional time_point parameter is\n"
                   "given, it will start loading objects from this time point. If the second optional\n"
						 "parameter is given, maximum of no_of_time_points is processed.\n";
		return(-1);
	}

	//process the config file
	if (mesh.configFile.ParseConfigFile(argv[1]) != 0) return(-2);
	//access the config params as, e.g. :
	// mesh.configFile.texture_Resolution.x

	//determine the first time point to start with
	int i=0;
	if (argc >= 6) i=atoi(argv[5]);

	int framesToGoHome=999999999;
	if (argc == 7) framesToGoHome=atoi(argv[6]);

	//get dataset ID
	int ID=atoi(argv[4]);

	//I'm called as generator/simulator, init the situation...
	ParamsSetup();

	std::cout << "FIRST RUN: reading all meshes to figure out minimal bounding box.\n";
	//doubling iteration variables not to loose their values
	int ii(i);                           //starting index
	int fframesToGoHome(framesToGoHome); //how many indices to read
	Vector3F minBB,maxBB;                //bounding box corners

	while ((fframesToGoHome > 0) && LoadNewMesh(argv[2],ID,ii))
	{
		std::cout << "mesh: #" << ii << "\n";
		std::cout << "vertices   #: " << mesh.Pos.size() << "\n";
		std::cout << "triangles  #: " << mesh.ID.size()/3 << "\n";
		std::cout << "tetrahedra #: " << mesh.VolID.size()/4 << "\n";
		std::cout << "normals    #: " << mesh.norm.size() << "\n";

		//std::cout << "filopodia #: " << mesh.Ftree.size() << "\n";
		for (size_t f=0; f < mesh.Ftree.size(); ++f)
		{
			if (mesh.Ftree[f].Pos.size() > 0)
			{
				std::cout << f << ": vertices  #: " << mesh.Ftree[f].Pos.size() << "\n";
				std::cout << f << ": triangles #: " << mesh.Ftree[f].ID.size()/3 << "\n";
				std::cout << f << ": normals   #: " << mesh.Ftree[f].norm.size() << "\n";
			}
		}

		//update the current bounding box
		if (i == ii)
		{
			//first run, init BB corners
			minBB=mesh.Pos[0];
			maxBB=mesh.Pos[0];
		}
		mesh.UpdateMeshBBox(minBB,maxBB);

		++ii;
		--fframesToGoHome;
	}

	//ii is the last timePoint -> allocate vector of cell centres large enough
	centreCoords.insert(centreCoords.end(),(unsigned)(ii)+1,Vector3F(-1.f));
	centreOffsets.insert(centreOffsets.end(),(unsigned)(ii)+1,-1);

	//given the discovered bounding box valid for the whole sequence, and given
	//that we DO want to keep mesh coordinates even in the produced images,
	//we have to utilize the images _offset_ attribute, instead of
	//translating all mesh points, to assure the meshes appear within
	//the produced images
	Vector3F borderSize( (maxBB-minBB)/100.f );
	borderSize.x *= mesh.configFile.texture_borderWidth.x;
	borderSize.y *= mesh.configFile.texture_borderWidth.y;
	borderSize.z *= mesh.configFile.texture_borderWidth.z;
	mesh.meshShift=minBB -borderSize;
	mesh.meshPlaygroundSize=(maxBB-minBB) +2.f*borderSize;
	//NB: We also introduce 1um thick border around the mesh
	std::cout << "corners of a detected bounding box:\n";
	std::cout << "[" << minBB.x << "," << minBB.y << "," << minBB.z << "] <-> ["
	          << maxBB.x << "," << maxBB.y << "," << maxBB.z << "] in microns\n";

	std::cout << "SECOND RUN: reading all meshes to conduct the texture simulation and rendering.\n";
	while ((framesToGoHome > 0) && LoadNewMesh(argv[2],ID,i,true))
	{
		RenderMesh(argv[3],ID,i);
		++i;
		--framesToGoHome;
	}

	//report the tip trajectories
	char fileName[1024];
	sprintf(fileName,"%s/ID%d_afterTM/tip_trajectories_pxOffsets.txt",argv[3],ID);
	std::ofstream oFile(fileName);
	sprintf(fileName,"%s/ID%d_afterTM/tip_trajectories_umCoords.txt",argv[3],ID);
	std::ofstream cFile(fileName);

	std::map<int,timeVector>::const_iterator tipIter=tipOffsets.begin();
	while (tipIter != tipOffsets.end())
	{
		oFile << tipIter->first << ":";
		cFile << tipIter->first << ":";
		for (int t=0; t < i; ++t)
		{
			oFile << tipIter->second.at((unsigned)t) << ",";
			cFile << tipCoords[tipIter->first].at((unsigned)t).x << ","
			      << tipCoords[tipIter->first].at((unsigned)t).y << ","
			      << tipCoords[tipIter->first].at((unsigned)t).z << ";";
		}
		oFile << "\n";
		cFile << "\n";
		++tipIter;
	}
	oFile.close();
	cFile.close();

	//report the cell centre coordinates
	sprintf(fileName,"%s/ID%d_afterTM/centre_trajectories_pxOffsets.txt",argv[3],ID);
	std::ofstream OFile(fileName);
	sprintf(fileName,"%s/ID%d_afterTM/centre_trajectories_umCoords.txt",argv[3],ID);
	std::ofstream CFile(fileName);

	OFile << "1:";
	CFile << "1:";
	for (unsigned int t=0; t < (unsigned)i; ++t)
	{
		//OFile << tipIter->second.at((unsigned)t) << ",";
		OFile << centreOffsets[t] << ",";
		CFile << centreCoords[t].x << ","
				<< centreCoords[t].y << ","
				<< centreCoords[t].z << ";";
	}
	OFile << "\n";
	CFile << "\n";

	OFile.close();
	CFile.close();

	return(0);
}


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);
}


bool LoadNewMesh(const char* path,const int ID,const int fileNo,const bool keepTrajectories)
{
	char fn[1024];

	//first, obtain a fresh volumetric mesh of the cell body
	sprintf(fn,"%s/ID%d/ID%d_t%03d_nucleus.vtk",path,ID,ID,fileNo);
	std::cout << "reading cell body: " << fn << "\n";

	//keep the volumetric configuration of the first load
	bool saveAlsoTetrahedra=(mesh.GetVolIDsize() == 0);
	int retval=mesh.ImportVTK_Volumetric(fn,saveAlsoTetrahedra);
	if (retval) 
	{
		//report errors except for "cannot open file"
		if (retval > 1) 
			std::cout << "some error reading cell body: " << retval << "\n";
		return(false);
	}

	//if we are now recording the trajectories,
	//record also the cell body centre
	if (keepTrajectories)
	{
		//scan all volumetric vertices and calc their centre
		double x=0.0;
		double y=0.0;
		double z=0.0;
		for (size_t i=0; i < mesh.Pos.size(); ++i)
		{
			x+=(double)mesh.Pos[i].x;
			y+=(double)mesh.Pos[i].y;
			z+=(double)mesh.Pos[i].z;
		}
		x/=double(mesh.Pos.size());
		y/=double(mesh.Pos.size());
		z/=double(mesh.Pos.size());

		//and save it
		centreCoords[(unsigned)fileNo]=Vector3F(float(x),float(y),float(z));
	}

	//start loading filopodia objects
	mesh.DiscardAllFilopodiaObjects();
	int filoNo=0;

	do
	{
		++filoNo;
		sprintf(fn,"%s/ID%d/ID%d_t%03d_fTree%02d.vtk",path,ID,ID,fileNo,filoNo);

		retval=mesh.ImportVTK_Ftree(fn,999999);
		if (retval != 1) std::cout << "reading filopodium: " << fn << "\n";
		if (retval == 0 && keepTrajectories == true)
		{
			//managed to import filopodium (possibly with its offspring branches)
			//remember at what indices in mesh.Ftree are these stored
			//(as we cannot yet remember directly the pixel offset, we at least
			//remember index within mesh.Ftree and translate to pixel offset later)
			//
			//iterate over the internal mapping (just_branchID -> Ftree indices)
			std::map<int,unsigned int>::const_iterator iterTips=mesh.tipOffsets.begin();
			while (iterTips != mesh.tipOffsets.end())
			{
				const int idx=tipIndex(filoNo,iterTips->first);
				if (tipOffsets[idx].size() == 0)
				{
					//we have found a new pair filoID+branchID
					tipOffsets[idx].Init();
					tipCoords[idx].Init();
				}
				tipOffsets[idx].at((unsigned)fileNo)=(signed)iterTips->second;

				++iterTips;
			}
		}
	} while (retval < 2 && filoNo < 100);
	//NB: retval == 0 when read successfully; == 1 when file open error

	//report errors except for "cannot open file"
	if (retval > 1) 
	{
		std::cout << "some error reading filopodium: " << retval << "\n";
		return(false);
	}

	//remove last filopodium mesh (which is usually empty...)
	mesh.DiscardLastEmptyFilopodiumObject();

	std::cout << "loaded mesh #" << fileNo << "\n";

	//keep (by rotating) vertex positions
	mesh.RotateVolPos();

	return(true);
}


#define DO_PHASE_II_AND_III
#define DO_ANISOTROPIC_OUTPUT
int RenderMesh(const char* path,const int ID,int fileNo)
{
	//setup isotropic(!) image for mask-output of the simulated cell
	i3d::Image3d<i3d::GRAY16> mask;
	mask.SetOffset(i3d::Offset(mesh.meshShift.x,mesh.meshShift.y,mesh.meshShift.z));
	const float isoRes = mesh.configFile.texture_Resolution.x;
	mask.SetResolution(i3d::Resolution(isoRes,isoRes,isoRes));
	//use "optimal" calculated size
	mask.MakeRoom((size_t)(mesh.meshPlaygroundSize.x*isoRes),
                 (size_t)(mesh.meshPlaygroundSize.y*isoRes),
                 (size_t)(mesh.meshPlaygroundSize.z*isoRes));
	mask.GetVoxelData()=0;

	//renders the isotropic mask into the image
	std::cout << "rendering mask #" << fileNo << "\n";
	mesh.RenderMask_bodyVol(mask); //full volume
	std::map<int,timeVector>::iterator tipIter=tipOffsets.begin();
	while (tipIter != tipOffsets.end())
	{
		bool checkCollisionOfTip = false;
		//if this filopodium existed already in the previous two frames, check for collision
		//NB: we do not want to check for collision for tips of filopodium that just appeared
		//    in the scene because this one is usually still "buried" inside its mother-filopodium;
		//    often its remains burried for the first two frames (of its life)
		if ((fileNo > 1) && (tipIter->second.at((unsigned)fileNo-2) > -1)) checkCollisionOfTip = true;
		//else std::cout << "RENDERING ID " << tipIter->first << " with no detection\n";

		if (tipIter->second.at((unsigned)fileNo) > -1)
			mesh.RenderMask_filo(mask,(unsigned)tipIter->second.at((unsigned)fileNo),(unsigned short)tipIter->first,false,checkCollisionOfTip);
		++tipIter;
	}

	std::cout << "exporting isotropic mask #" << fileNo << "\n";
	char fileName[1024];
	sprintf(fileName,"%s/ID%d_afterTM/ISOmask_t%03d.tif",path,ID,fileNo);
	mask.SaveImage(fileName);

	//updates the texture fl. points
	if (mesh.DotsFirstRun())
	{
		//we are now simulating the very first image in the sequence,
		//we need to initiate the textures
		//
		//first, prepare the cell body texture
		std::cout << "rendering texture first run\n";
		mesh.InitDots_body(mask);
		//slightly displace the fl. points to "hide" the FF shift-effects
		//NB: the filopodia texture dots are now empty
		mesh.BrownDots(mask);

		//second, create texture for filopodia
		mesh.dots_filo.clear();
		mesh.dots_filo.reserve(300000);
		for (unsigned int i=0; i < mesh.GetFilopodiaNumber(); ++i)
			mesh.InitDots_filo(mask,i); //filopodium one by one
		std::cout << "filo dots size=" << mesh.dots_filo.size() << "\n";
	}
	else
	{
		//we are now simulating some next image, we should update the
		//previously created texture (at least for the cell body)
		std::cout << "rendering texture iteration\n";

		//internal-affairs (movement due to cell will, internal processes)

		//this is not considered in this project
		//mesh.BrownDots(mask);


		//external-affairs (movement due to cell movement itself)

		//obtain fresh flow field, so that point positions can be updated
		std::cout << "oldVol.size=" << mesh.oldVolPos.size() << ", newVol.size=" << mesh.newVolPos.size() << "\n";

		FlowField<float> FF;
		FF.x=new i3d::Image3d<float>;
		FF.y=new i3d::Image3d<float>;
		FF.z=new i3d::Image3d<float>;
		FF.x->CopyMetaData(mask);
		FF.y->CopyMetaData(mask);
		FF.z->CopyMetaData(mask);

		//create a FF (FlowField) by "rendering displacement triangles"
		mesh.ConstructFF_T(FF);
/*
		//uncomment if you want to see the FF for the cell body
		char n[1024];
		sprintf(n,"%s/ID%d_afterTM/new_vx_%03d.ics",path,ID,fileNo);
		FF.x->SaveImage(n);
		sprintf(n,"%s/ID%d_afterTM/new_vy_%03d.ics",path,ID,fileNo);
		FF.y->SaveImage(n);
		sprintf(n,"%s/ID%d_afterTM/new_vz_%03d.ics",path,ID,fileNo);
		FF.z->SaveImage(n);
*/

		//apply the FF on the fl. dots
		mesh.FFDots(mask,FF);

		//FF destructor will delete dyn. allocated images in the FF...

		//also update filopodia (by re-creating them from the scratch)
		mesh.dots_filo.clear();
		mesh.dots_filo.reserve(300000);
		for (unsigned int i=0; i < mesh.GetFilopodiaNumber(); ++i)
			mesh.InitDots_filo(mask,i); //filopodium one by one
		std::cout << "filo dots size=" << mesh.dots_filo.size() << "\n";
	}

	//now the texture is fresh and in place of the new mask

	//renders the isotropic texture
	std::cout << "rendering texture I   #" << fileNo << "\n";
	i3d::Image3d<i3d::GRAY16> texture;
	mesh.RenderDots(mask,texture);
	//
	//or always one shot (for debugging, always a new texture, incoherent):
	//i3d::Image3d<i3d::GRAY16> texture;
	//mesh.RenderOneTimeTexture(mask,texture);

	sprintf(fileName,"%s/ID%d_afterTM/phantom_t%03d.tif",path,ID,fileNo);
	texture.SaveImage(fileName);

#ifdef DO_PHASE_II_AND_III
	//do phase II on the isotropic image
	std::cout << "rendering texture II  #" << fileNo << "\n";
	i3d::Image3d<float> intermediate;
	mesh.PhaseII(texture,intermediate);

 #ifdef DO_ANISOTROPIC_OUTPUT
	//downsample now:
	const float factor[3]={1.f, //NB: isotropic phantom has resolution of the x-axis
	  mesh.configFile.texture_Resolution.y/mesh.configFile.texture_Resolution.x,
	  mesh.configFile.texture_Resolution.z/mesh.configFile.texture_Resolution.x};
	i3d::Image3d<float>* tmp=NULL;
	i3d::lanczos_resample(&intermediate,tmp,factor,2);
	intermediate=*tmp;
	delete tmp;
 #endif

	//do phase III on the final image (can be iso or aniso now)
	std::cout << "rendering texture III #" << fileNo << "\n";
	mesh.PhaseIII(intermediate,texture);

	//save the texture and that's it for texture image
	std::cout << "exporting texture #" << fileNo << "\n";
	sprintf(fileName,"%s/ID%d_afterTM/texture_t%03d.tif",path,ID,fileNo);
	texture.SaveImage(fileName);
#endif


	std::cout << "exporting mask #" << fileNo << "\n";
	sprintf(fileName,"%s/ID%d_afterTM/mask_t%03d.tif",path,ID,fileNo);
#ifdef DO_PHASE_II_AND_III
 #ifdef DO_ANISOTROPIC_OUTPUT
	//re-render (!, instead of resampling) also the mask before exporting
	//new size (lanczos_resample() rounds size, we must too)
	mask.MakeRoom((size_t)roundf(factor[0]*mask.GetSizeX()),
	              (size_t)roundf(factor[1]*mask.GetSizeY()),
	              (size_t)roundf(factor[2]*mask.GetSizeZ()));
	//new resolution
	i3d::Resolution res=mask.GetResolution();
	res.SetX(factor[0]*res.GetX());
	res.SetY(factor[1]*res.GetY());
	res.SetZ(factor[2]*res.GetZ());
	mask.SetResolution(res);
	//re-render
	mask.GetVoxelData()=0;
	mesh.RenderMask_bodyVol(mask); //full volume
	tipIter=tipOffsets.begin();
	while (tipIter != tipOffsets.end())
	{
		if (tipIter->second.at((unsigned)fileNo) > -1)
			mesh.RenderMask_filo(mask,(unsigned)tipIter->second.at((unsigned)fileNo),(unsigned short)tipIter->first);
		++tipIter;
	}
 #endif
#endif
	//save the mask image
	mask.SaveImage(fileName);

	//record voxel offsets within a mask image of all filopodia tips
	const float xRes=mask.GetResolution().GetX();
	const float yRes=mask.GetResolution().GetY();
	const float zRes=mask.GetResolution().GetZ();
	const float xOff=mask.GetOffset().x;
	const float yOff=mask.GetOffset().y;
	const float zOff=mask.GetOffset().z;

	//scan over all branches we recognize at this time-point
	tipIter=tipOffsets.begin();
	while (tipIter != tipOffsets.end())
	{
		//if there is a record for the present time-point, translate it
		if (tipIter->second.at((unsigned)fileNo) > -1)
		{
			const unsigned int i=(unsigned)tipIter->second.at((unsigned)fileNo);
			const Vector3FC& realPos=mesh.Ftree[i].fPoints[mesh.Ftree[i].segToPoint.back()];
			//get pixel coordinate
			const size_t x=size_t((realPos.x-xOff) *xRes);
			const size_t y=size_t((realPos.y-yOff) *yRes);
			const size_t z=size_t((realPos.z-zOff) *zRes);
			tipIter->second.at((unsigned)fileNo)=(int)mask.GetIndex(x,y,z);

			//save also the micron coordinate at the corresponding place
			//into the coordinate structure
			tipCoords[tipIter->first].at((unsigned)fileNo)=realPos;
		}
		++tipIter;
	}

	//translate cell centre into offset:
	//get pixel coordinate
	const size_t x=size_t((centreCoords[(unsigned)fileNo].x-xOff) *xRes);
	const size_t y=size_t((centreCoords[(unsigned)fileNo].y-yOff) *yRes);
	const size_t z=size_t((centreCoords[(unsigned)fileNo].z-zOff) *zRes);
	centreOffsets[(unsigned)fileNo]=(int)mask.GetIndex(x,y,z);

	return(0);
}
