/// Copyright (c) 2010 Ruben Penalva. All rights reserved.
/// Redistribution and use in source and binary forms, with or without modification, 
/// are permitted provided that the following conditions are met:
///    1. Redistributions of source code must retain the above copyright notice, this 
///    list of conditions and the following disclaimer.
///    2. Redistributions in binary form must reproduce the above copyright notice,
///    this list of conditions and the following disclaimer in the documentation and/or
///    other materials provided with the distribution.
///    3. The name of the author may not be used to endorse or promote products derived
///    from this software without specific prior written permission.
///
/// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
/// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 
/// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, 
/// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 
/// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
/// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
/// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
/// THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

/// @file Render.cpp
/// This source file is part of Render CUDA Ray Casting test.
/// The file holds the Render class implementation.
/// @author Ruben Penalva (ruben.penalva@rpenalva.com)
#include "Render.h"

#include <GL/glew.h>
#include <GL/glut.h>
#include <GL/gl.h>
#include <GL/glu.h>
#include <math.h>
#include <assert.h>

#include <cuda_runtime_api.h>
#include <cuda_gl_interop.h>


using namespace CUDARayCasting;

extern "C" 
void CallCUDAKernel(dim3 gridDim, dim3 blockDim, 
						   unsigned int* pixelBuffer, int pbWidth, int pbHeight, 
						   float* cameraPos, float* invViewMatrix, float* invVolMatrix,
						   float* bboxMin, float* bboxMax, int steps, float step);

extern "C" 
void CreateCUDAVolume(int texWidth, int texHeight, int texDepth, unsigned char* dataPtr);

extern "C" 
void CreateCUDATransferFunction(float* transferFunctionPtr, int transferFunctionWidth);

extern "C"
void UpdateCUDATransferFunction(float* transferFunctionPtr, int transferFunctionWidth);

extern "C"
void DestroyCudaVolume(void);

extern "C"
void DestroyCUDATransferFunction(void);

Render::Render(int width, int height)	:	m_width(width), m_height(height)
{
}

Render::~Render(void)
{	
	DestroyCudaVolume();

	DestroyCUDATransferFunction();

	cudaThreadExit();
}

void Render::Initialize(boost::shared_array<unsigned char> dataPtr, 
						int texWidth, int texHeight, int texDepth,
						boost::shared_array<float> transferFunctionPtr,
						int transferFunctionWidth, float step, int stepNumber,
						const Matrix4x4& viewMatrix, const Matrix4x4& volumeMatrix,  
						const Vector3& bboxMin, const Vector3& bboxMax)
{
	GLenum err = glewInit();
	if (GLEW_OK != err)
	{
		/// glewGetErrorString(err)
		throw L"[ERROR] Error in glew initialization. Render constructor.";
	}

	if (!GLEW_VERSION_2_1)
	{
		throw L"[ERROR] Opengl 2.1 required.";
	}

	/// TODO
	/// Check CUDA capabilities

	m_viewMatrix = viewMatrix;
	m_volumeMatrix = volumeMatrix;

	m_transferFunctionPtr = transferFunctionPtr;

	m_bboxMin = bboxMin;
	m_bboxMax = bboxMax;

	m_steps = stepNumber;
	m_step = step;

	m_transferFunctionWidth = transferFunctionWidth;

	m_pixelBuffer.reset(new PixelBuffer(m_width, m_height, sizeof(GLubyte), 4));

	glEnable(GL_TEXTURE_3D);

	cudaGLSetGLDevice(0);
	cudaGLRegisterBufferObject(m_pixelBuffer->GetId());

	CreateCUDAVolume(texWidth, texHeight, texDepth, dataPtr.get());

	CreateCUDATransferFunction(transferFunctionPtr.get(), transferFunctionWidth);
}

void Render::Reshape(int width, int height)
{
	//glClearColor(0.5, 0.5, 0.7, 0.0);

	//glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	//glEnable(GL_CULL_FACE);
	//
	glViewport(0, 0, width, height);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0);

	m_width = width;
	m_height = height;
}

void Render::BeginFrame(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

void Render::EndFrame(void)
{
	glutSwapBuffers();
}

void Render::DrawVolume(void)
{
	void* d_viewMatrix = 0;
	cudaMalloc(&d_viewMatrix, sizeof(Matrix4x4));
	Matrix4x4 viewInvMatrix = m_viewMatrix.Inverse();
	cudaMemcpy(d_viewMatrix, viewInvMatrix.GetData(), sizeof(Matrix4x4), cudaMemcpyHostToDevice) ;

	void* d_volumeMatrix = 0;
	cudaMalloc(&d_volumeMatrix, sizeof(Matrix4x4));
	Matrix4x4 volumeInvMatrix = m_volumeMatrix.Inverse();
	cudaMemcpy(d_volumeMatrix, volumeInvMatrix.GetData(), sizeof(Matrix4x4), cudaMemcpyHostToDevice) ;

	void* d_cameraPos = 0;
	cudaMalloc(&d_cameraPos, sizeof(Vector3));
	Vector3 cameraPos = volumeInvMatrix * viewInvMatrix * Vector3();
	
	cudaMemcpy(d_cameraPos, cameraPos.m_data, sizeof(Vector3), cudaMemcpyHostToDevice) ;

	void* d_bboxMin = 0;
	cudaMalloc(&d_bboxMin, sizeof(Vector3));
	cudaMemcpy(d_bboxMin, m_bboxMin.m_data, sizeof(Vector3), cudaMemcpyHostToDevice) ;
	void* d_bboxMax = 0;
	cudaMalloc(&d_bboxMax, sizeof(Vector3));
	cudaMemcpy(d_bboxMax, m_bboxMax.m_data, sizeof(Vector3), cudaMemcpyHostToDevice) ;

	unsigned int* d_pixelbuffer;
	cudaGLMapBufferObject((void**)&d_pixelbuffer, m_pixelBuffer->GetId());
	cudaMemset(d_pixelbuffer, 0, m_width * m_height * 4);

	dim3 gridDim((m_width % 16 != 0) ? (m_width / 16 + 1) : (m_width / 16),
				 (m_height % 16 != 0) ? (m_height / 16 + 1) : (m_height / 16));
	dim3 blockDim(16, 16);
	CallCUDAKernel(gridDim, blockDim, d_pixelbuffer, m_width, m_height, 
				   reinterpret_cast<float*>(d_cameraPos),
				   reinterpret_cast<float*>(d_viewMatrix),
				   reinterpret_cast<float*>(d_volumeMatrix),
				   reinterpret_cast<float*>(d_bboxMin), 
				   reinterpret_cast<float*>(d_bboxMax), 
				   m_steps, m_step);
	
	cudaGLUnmapBufferObject(m_pixelBuffer->GetId());

	cudaFree(d_bboxMin);
	cudaFree(d_bboxMax);
	cudaFree(d_cameraPos);
	cudaFree(d_viewMatrix);

	glDisable(GL_DEPTH_TEST);
	glRasterPos2i(0, 0);
	glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, m_pixelBuffer->GetId());
	glDrawPixels(m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, 0);
	glBindBufferARB(GL_PIXEL_UNPACK_BUFFER_ARB, 0);
}

void Render::UpdateTransferFunction(void)
{
	UpdateCUDATransferFunction(m_transferFunctionPtr.get(), m_transferFunctionWidth);
}

void Render::SetStep(float step)
{
	m_step = step;
}

void Render::SetStepNumber(int stepNumber)
{
	m_steps = stepNumber;
}

void Render::DrawString(const std::string& s, int x, int y, const Vector3& color)
{
	glUseProgram(0);
	glDisable(GL_TEXTURE_3D);

	glActiveTexture(GL_TEXTURE0);

	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	gluOrtho2D(0.0, m_width, 0.0, m_height);

	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();

	glColor3f(color.m_x, color.m_y, color.m_z);

	glRasterPos2i(x, y);

	std::string::const_iterator it = s.begin();
	std::string::const_iterator itEnd = s.end();
	while (it != itEnd)
	{
		glutBitmapCharacter(GLUT_BITMAP_9_BY_15, *it);

		++it;
	}

	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();

	glMatrixMode(GL_PROJECTION);
	glPopMatrix();

	glEnable(GL_TEXTURE_3D);
}

void Render::SetCamera(const Matrix4x4& viewMatrix)
{
	m_viewMatrix = viewMatrix;
}

void Render::SetVolumeMatrix(const Matrix4x4& volumeMatrix)
{
	m_volumeMatrix = volumeMatrix;
}
