/*
* Copyright 1993-2010 NVIDIA Corporation.  All rights reserved.
*
* NVIDIA Corporation and its licensors retain all intellectual property and 
* proprietary rights in and to this software and related documentation. 
* Any use, reproduction, disclosure, or distribution of this software 
* and related documentation without an express license agreement from
* NVIDIA Corporation is strictly prohibited.
*
* Please refer to the applicable NVIDIA end user license agreement (EULA) 
* associated with this source code for terms and conditions that govern 
* your use of this NVIDIA software.
* 
*/

/*	
Class to encapsulate the data used to stress the CopyEngine - 
Can be 2d image series (video) or 3d volume time-series
*/

#include <GL/glew.h>
#include <windows.h>

#include <assert.h>
#include <time.h>
#include "Data.h"
#include "nvThread.h"
#include "TextureSlicer.h"
//defined in glut_teapot.cpp
extern void glutWireTeapot(GLdouble scale);
extern void glutSolidTeapot(GLdouble scale);

TextureSlicer volrenderer; //the volume renderer we use if we have a series of 3d volumes
//TODO
//VideoCompositor imgrenderer; //the kind of image renderer we use if we have a series of 2d images


Data::Data(const char* filename, int width, int height, int depth, int timesteps, TransferMode downloadMode)
: m_width(width),
m_height(height),
m_depth(depth),
m_curTimeStep(0),
m_curRender(0),
m_curDownload(0),
m_downloadMode(downloadMode)
{
	m_target = ((depth>1)?GL_TEXTURE_3D:GL_TEXTURE_2D);
	if (m_downloadMode == TRANSFER_GPUASYNC) {
		for (int i=0;i<numDownloadBuffers;i++) {
			evStartDownload[i] = nvCreateEvent();
			evDownloadFenceCreated[i] = nvCreateEvent();
		}
	}
	_init(filename, timesteps);
}

Data::~Data() {
	for (int i=0;i<m_pData.size();i++)
		delete [] m_pData[i];
	m_pData.clear();
	glDeleteTextures(numDownloadBuffers,m_Tex);
	glDeleteBuffersARB(1,&m_pbo);
	if (m_downloadMode == TRANSFER_GPUASYNC) {
		for (int i=0;i<numDownloadBuffers;i++) {
			nvDestroyEvent(evStartDownload[i]); evStartDownload[i] = NULL; //Destroy the 2 events
			nvDestroyEvent(evDownloadFenceCreated[i]); evDownloadFenceCreated[i] = NULL;	
		}
	}
}

void Data::startRender() {
	if (m_downloadMode == TRANSFER_GPUASYNC) {
		nvWaitForEvent(evDownloadFenceCreated[m_curRender]);//wait for fence to be created so that we can wait on it
		//printf("Data::startRender Starting wait for download fence %d\n",m_curRender);
		glWaitSync(downloadFence[m_curRender], 0, GL_TIMEOUT_IGNORED); //arg 2 and 3 must be 0 and GL_TIMEOUT_IGNORED resp
	}
	else if ((m_downloadMode == TRANSFER_CPUASYNC)||(m_downloadMode == TRANSFER_SYNC))  {
		step();
	}
}

void Data::render() {
	if (m_target == GL_TEXTURE_3D) {
		//have a 3d texture, lets volume render
		volrenderer.render(this);
	}
	else {//2d texture
		//temp - just generate some geometry to affect the rendering time.
		//TODO make the rendering more meaningful
		//imgrenderer.render(this);
		for (int i=0;i<10;i++)
			glutWireTeapot(0.5f);
		glEnable(m_target);
		glBindTexture(m_target, m_Tex[m_curRender]);
		glBegin(GL_QUADS);
		glTexCoord2f(0.0, 0.0); glVertex3f(-1.0, -1.0, 0.0);
		glTexCoord2f(0.0, 1.0); glVertex3f(-1.0, 1.0, 0.0);
		glTexCoord2f(1.0, 1.0); glVertex3f(1.0, 1.0, 0.0);
		glTexCoord2f(1.0, 0.0); glVertex3f(1.0, -1.0, 0.0);
		glEnd();
		glDisable(m_target);
	}
}

void Data::endRender() {
	if (m_downloadMode == TRANSFER_NONE)
		return;
	if (m_downloadMode == TRANSFER_GPUASYNC) {
		glDeleteSync(downloadFence[m_curRender]);		
		nvSignalEvent(evStartDownload[m_curRender]); //Tell download thread to start filling texture when it gets to it
		//printf("Data::endRender Signal done with %d\n",m_curRender);
	}
	m_curRender = (m_curRender+1)%numDownloadBuffers;
}

//Step to the next time step, if using CE this is called by the download thread
void Data::step(float deltaTime) {
	//do nothing for single time step
	if (m_pData.size() ==1)
		return;
	m_curTimeStep = (m_curTimeStep+1)%m_pData.size();  //cycle back to 0 if last time step
	_downloadToTexture();

}

//================= PROTECTED FUNCTIONS BELOW ==========================
//init data - randome data for the series of 2d textures, for 3d textures, we use a filename
//TODO - cleanuppppp
void Data::_init(const char* filename, int timesteps)
{
	m_pData.resize(timesteps);
	//for 3d texture we only have 1 component but 2d images, we have rgba or 4 components
	int nComponents = ((m_target == GL_TEXTURE_3D)?1:4);
	unsigned int size = m_width*m_height*m_depth*nComponents;
	srand (time(NULL) ); //seed
	char lfilename[256];
	for (unsigned int t=0;t<m_pData.size();t++) { //iterate over timesteps
		m_pData[t] = new unsigned char[size];
		if (filename) { //only supported file is raw
			if (m_pData.size() >1) {
				if (t<10)
					sprintf(&lfilename[0],"%s00%d.raw",filename,t);
				else if (t<100)
					sprintf(&lfilename[0],"%s0%d.raw",filename,t);
				else
					sprintf(&lfilename[0],"%s%d.raw",filename,t);
			}
			else
				strcpy(lfilename,filename);
			FILE *pFile = fopen(lfilename,"rb");
			printf("Opening file %s\n",lfilename);
			assert(pFile!=NULL);
			bool ok = (size == fread(m_pData[t],sizeof(unsigned char), size,pFile));
			fclose(pFile);
		}
		else { //TODO also add 3d noise genration
			//asumming m_depth = 1 for now
			assert(m_depth == 1);// we do not support 3d tex here yuck! change this
			for (unsigned int i = 0; i < m_height; i++) {
				for (unsigned int j = 0; j < m_width; j++) {
					int base = (i*m_width+j)*nComponents;
					m_pData[t][base] = m_pData[t][base+1] = m_pData[t][base+2] = ((float)rand()/RAND_MAX)*255;
					m_pData[t][base+3] = (GLubyte) 255;
				}
			}
		} //end of if then else filename
	}

	//Create numDownloadbuffers textures  and initialize with the time steps
	//TODO - check that numdownloadbuffers < ntimesteps
	glGenTextures(numDownloadBuffers,m_Tex); 
	for (int i=0;i<numDownloadBuffers;i++) {
		glBindTexture(m_target,m_Tex[i]);
		glTexParameteri(m_target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
		glTexParameteri(m_target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
		if (m_target == GL_TEXTURE_3D)
			glTexParameteri(m_target, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER);
		glTexParameteri(m_target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(m_target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		if (m_target == GL_TEXTURE_3D)
			glTexImage3D(GL_TEXTURE_3D,0,GL_INTENSITY8,m_width,m_height,m_depth,0,GL_LUMINANCE,GL_UNSIGNED_BYTE,m_pData[i]);
		else
			glTexImage2D(m_target,0,GL_RGBA8,m_width,m_height,0,GL_RGBA,GL_UNSIGNED_BYTE,m_pData[i]);
	}

	//Initialize PBO with 1st time step , TODO add ping pong pbo's and see if that helps
	glGenBuffersARB(1, &m_pbo); 
	GLubyte* ptr = 0;
	glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, m_pbo);
	glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB, size*sizeof(GLubyte) , NULL, GL_STREAM_DRAW_ARB);
	ptr = (GLubyte*)glMapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, GL_WRITE_ONLY_ARB);
	assert(ptr);
	memcpy(ptr,m_pData[0],size*sizeof(GLubyte));
	glUnmapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB);
	glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);

	//for all downloaders waiting, tell them to start
	if (m_downloadMode == TRANSFER_GPUASYNC) {
		for (int i=0;i<numDownloadBuffers;i++)
			nvSignalEvent(evStartDownload[i]); //Tell download threads to start downloading to all textures
	}
	//TODO step to initialize all the textures
	//	for (int i=0; i < numDownloadBuffers; i++)
	//		step();
}

void
Data::_downloadToTexture()
{
	if ((m_downloadMode == TRANSFER_CPUASYNC)||(m_downloadMode == TRANSFER_GPUASYNC)) {
		int nComponents = ((m_target == GL_TEXTURE_3D)?1:4);
		unsigned int size = m_width*m_height*m_depth*nComponents;

		//1. Copy pixels from pbo to texture object, need texture ref, make sure the render thread is done with it
		if (m_downloadMode == TRANSFER_GPUASYNC) { //only if we are using copy engines
			nvWaitForEvent(evStartDownload[m_curDownload]); //from main thread after it has used the texture for rendering
			//printf("Data:_downloadToTexture got permission to download %d\n",m_curDownload);
		}
		glBindTexture(m_target,m_Tex[m_curDownload]);
		glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, m_pbo); //bind pbo
		if (m_target == GL_TEXTURE_2D) {
			glTexSubImage2D(GL_TEXTURE_2D, 0, 
				0, 0, m_width, m_height,GL_RGBA, GL_UNSIGNED_BYTE, 0);
		}
		else {
			glTexSubImage3D(GL_TEXTURE_3D, 0, 
				0, 0, 0, 
				m_width, m_height, m_depth, 
				GL_LUMINANCE, GL_UNSIGNED_BYTE, 0);
		}

		if (m_downloadMode == TRANSFER_GPUASYNC) { //only if we are using copy engines 
			// Our master can wait on this new fence.
			downloadFence[m_curDownload] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
			glFlush();
			// Tell the master that fence is now valid to use.
			nvSignalEvent(evDownloadFenceCreated[m_curDownload]);
			//printf("Data: Render can wait on fence %d\n",m_curDownload);	
		}
		m_curDownload = (m_curDownload+1)%numDownloadBuffers;

		//2. App -> pbo transfer
		//to prevent sync issue in case GPU is till working with the data
		glBufferDataARB(GL_PIXEL_UNPACK_BUFFER_ARB, size*sizeof(GLubyte), 0, GL_STREAM_DRAW_ARB); 
		GLubyte* ptr = (GLubyte*)glMapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, GL_WRITE_ONLY_ARB);
		//This is where app writes data
		assert(ptr);
		memcpy(ptr,m_pData[m_curTimeStep],size*sizeof(GLubyte));
		glUnmapBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB);

		glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB,0);
	}
	else if (m_downloadMode == TRANSFER_SYNC){
		glBindTexture(m_target,m_Tex[m_curDownload]);
		if (m_target == GL_TEXTURE_2D) 
			glTexSubImage2D(GL_TEXTURE_2D, 0, 
			0, 0, m_width, m_height,GL_RGBA, GL_UNSIGNED_BYTE, m_pData[m_curTimeStep]);
		else
			glTexSubImage3D(GL_TEXTURE_3D, 0, 
			0, 0, 0, 
			m_width, m_height, m_depth, 
			GL_LUMINANCE, GL_UNSIGNED_BYTE, m_pData[m_curTimeStep]);

	} //end of if use pbo
}

//void Data::genData() {
//	for (unsigned int k = 0; k < m_depth; k++) {
//		for (unsigned int i = 0; i < m_height; i++) {
//			for (unsigned int j = 0; j < m_width; j++) {
//				GLuint c = ((((i&0x8)==0)^((j&0x8))==0))*255.0;
//				int base = (k*m_width*m_height+i*m_width+j)*nComponents;
//				if (m_target == GL_TEXTURE_3D) {
//					m_pData[t][base] = (GLubyte) c;
//				}
//				else {
//					m_pData[t][base] = m_pData[t][base+1] = m_pData[t][base+2] = (GLubyte) c;
//					m_pData[t][base+3] = (GLubyte) 255;
//				}
//			}
//		}
//	}
//}
//Noise generation functions are below
//helper functions for generating random 2d textures
//#define noiseWidth 128
//#define noiseHeight 128
//double noise[noiseWidth][noiseHeight]; //the noise array
//
//void generateNoise()
//{
//    for (int x = 0; x < noiseWidth; x++)
//    for (int y = 0; y < noiseHeight; y++)
//    {
//        noise[x][y] = (rand() % 32768) / 32768.0;
//    }
//}
//
//
//double smoothNoise(double x, double y)
//{  
//   //get fractional part of x and y
//   double fractX = x - int(x);
//   double fractY = y - int(y);
//   
//   //wrap around
//   int x1 = (int(x) + noiseWidth) % noiseWidth;
//   int y1 = (int(y) + noiseHeight) % noiseHeight;
//   
//   //neighbor values
//   int x2 = (x1 + noiseWidth - 1) % noiseWidth;
//   int y2 = (y1 + noiseHeight - 1) % noiseHeight;
//
//   //smooth the noise with bilinear interpolation
//   double value = 0.0;
//   value += fractX       * fractY       * noise[x1][y1];
//   value += fractX       * (1 - fractY) * noise[x1][y2];
//   value += (1 - fractX) * fractY       * noise[x2][y1];
//   value += (1 - fractX) * (1 - fractY) * noise[x2][y2];
//
//   return value;
//} 
//
//double turbulence(double x, double y, double size)
//{
//    double value = 0.0, initialSize = size;
//    while(size >= 1)
//    {
//        value += smoothNoise(x / size, y / size) * size;
//        size /= 2.0;
//    }
//    
//    return(128.0 * value / initialSize);
//} 
//
