#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:
std::vector< std::vector<int> > tipOffsets;

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

int main(int argc,char **argv)
{
	if (argc != 4 && argc != 5 && argc != 6)
	{
		//I'm executed with no params, provide help
		std::cout << "ADD SOME HEADER INFORMATION HERE\n\n";
		std::cout << "params:  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);
	}

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

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

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

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

	//for filopodia tip tracking:
	//container for 200 (all possible) filopodia tips,
	//initiated for 200 timeframes with position undefined (yet)
	tipOffsets.reserve(200);
	for (unsigned int qq=0; qq < 200; ++qq)
		tipOffsets.push_back( std::vector<int>(200,-1) );

	//indicator that the mesh translation has not been decided yet
	mesh.meshShift.x=-4567.f;

	while (LoadNewMesh(argv[1],ID,i) && (framesToGoHome > 0))
	{
		std::cout << "mesh: #" << i << "\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)
		{
			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";
		}

		RenderMesh(argv[2],ID,i);
		++i;
		--framesToGoHome;
	}

	//report the tip trajectories
	std::ofstream tFile("tip_trajectories.txt");
	for (unsigned int qq=0; qq < mesh.GetFilopodiaNumber(); ++qq)
	{
		tFile << qq << ":";
		for (int t=0; t < i; ++t)
			tFile << tipOffsets[qq][(unsigned)t] << ",";
		tFile << "\n";
	}
	tFile.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)
{
	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);
	}

	//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);
		std::cout << "reading filopodium: " << fn << "\n";

		retval=mesh.ImportVTK_Ftree(fn,999999);
	} while (retval == 0);

	//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 << " (with " << filoNo-1 << " filopodia)\n";

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

	//position the mesh somewhat inside the scene
	//first run?
	if (mesh.meshShift.x == -4567.f) mesh.CenterMesh();
	//translate...
	mesh.TranslateMesh(mesh.meshShift);

	//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(0.0,0.0,0.0));
	mask.SetResolution(i3d::Resolution(8.0,8.0,8.0));
	//use "optimal" calculated size
	mask.MakeRoom((size_t)(mesh.meshPlaygroundSize.x*8.0f),
                 (size_t)(mesh.meshPlaygroundSize.y*8.0f),
                 (size_t)(mesh.meshPlaygroundSize.z*8.0f));
	//over-ride size:
	//mask.MakeRoom(220,220,176);
	mask.GetVoxelData()=0;

	//renders the isotropic mask into the image
	std::cout << "rendering mask #" << fileNo << "\n";
	mesh.RenderMask_bodyVol(mask); //full volume
	for (unsigned int i=0; i < mesh.GetFilopodiaNumber(); ++i)
		mesh.RenderMask_filo(mask,i);

	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,1.f,0.125f};
	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);
#endif

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


	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
	for (unsigned int i=0; i < mesh.GetFilopodiaNumber(); ++i)
		mesh.RenderMask_filo(mask,i);
 #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;
	for (unsigned int i=0; i < mesh.GetFilopodiaNumber(); ++i)
	{
		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);
		tipOffsets[i][(unsigned)fileNo]=(int)mask.GetIndex(x,y,z);
	}

	return(0);
}
