#include "stdafx.h"
#include "volumerenderer_private.h"

/*!
\file
This file contains all initialization-related stuff of the VolumeRenderer for more clean looks. The main implementation file is volumerenderer.cpp.
\sa volumerenderer_private.h
*/

VolumeRenderer::VolumeRenderer(QObject *parent)
	: QObject(parent),d(*new VolumeRendererPrivate)
{
	//create a new volume loader for us
	d.volLoader = new VolumeLoader(this);
}

VolumeRenderer::~VolumeRenderer()
{
	delete &d;
}


#define FRAGDATA_LOCATION "fragColor"

void VolumeRenderer::processLogMessage(const QOpenGLDebugMessage& msg)
{
	qWarning()<<"GL error: "<<msg;
}

PFNGLBINDIMAGETEXTUREPROC __glBindImageTexture;
PFNGLDISPATCHCOMPUTEPROC __glDispatchCompute;

bool VolumeRenderer::checkGLRequirements()
{
	qDebug()<<"Checking GL requirements";
	QOpenGLContext* curr = QOpenGLContext::currentContext();

#define CHECK_EXT(ctx,str) if(d.glExtensions.contains(str)) \
	{ qDebug()<<str<<"... ok"; } \
else { qCritical()<<str<<"... MISSING!";  m_errString=tr("Required OpenGL extension %1 not available, sorry :/").arg(str); return false;}

	//only checking most advanced stuff required
	CHECK_EXT(curr,"GL_ARB_geometry_shader4");
	CHECK_EXT(curr,"GL_ARB_compute_shader");
	CHECK_EXT(curr,"GL_ARB_shader_image_load_store");

	//setup missing entry points
	__glBindImageTexture = (PFNGLBINDIMAGETEXTUREPROC)curr->getProcAddress("glBindImageTexture");
	__glDispatchCompute = (PFNGLDISPATCHCOMPUTEPROC)curr->getProcAddress("glDispatchCompute");

	return true;
}

bool VolumeRenderer::initialize()
{
	d.timer.start();

	//TODO check if all required extensions exist
	qDebug()<<"VolumeRenderer initialize";

	//this logger would be quite nice, but it has a bug on ATI cards that leads to crashing. Fixed in 5.3, but thats not out yet.
	//if we really need this now, we can hook GL_KHR_debug ourselves
	/*
	//attach an qt opengl debug logger for easy debugging
	d.glLogger = new QOpenGLDebugLogger(this);

	bool ok = d.glLogger->initialize();
	if(!ok)
	{
	qWarning()<<"Qt OpenGL debug logger could not be initialized, the GL_KHR_debug extension is probably not supported";
	}
	else
	{
	//QObject::connect(d.glLogger,SIGNAL(messageLogged(const& QOpenGLDebugMessage)),
	//this, SLOT(processLogMessage(const& QOpenGLDebugMessage)));
	d.glLogger->startLogging(QOpenGLDebugLogger::SynchronousLogging);
	qDebug()<<"Qt OpenGL debug logger added";
	}
	*/

	bool ok = this->initializeOpenGLFunctions();	
	if(!ok)
	{
		qWarning()<<"initializeOpenGLFunctions returned false, something may be broken...";
	}
	else
	{
		qDebug()<<"All OpenGL 3.3 core functions initialized";
	}

	qDebug()<<"OpenGL information dump:"<<endl
		<<"GL vendor: "<<(char*)glGetString(GL_VENDOR)<<endl
		<<"GL renderer: "<<(char*)glGetString(GL_RENDERER)<<endl
		<<"GL version: "<<(char*)glGetString(GL_VERSION)<<endl
		<<"GLSL version: "<<(char*)glGetString(GL_SHADING_LANGUAGE_VERSION);

	QString s;
	int numExt;
	glGetIntegerv(GL_NUM_EXTENSIONS,&numExt);
	for(int i =0;i<numExt;++i)
	{
		char* str =(char*)glGetStringi(GL_EXTENSIONS,i);
		d.glExtensions.append(QString(str).trimmed());
		s.append(str);
		s.append(' ');
	}
	qDebug()<<"Extensions supported: "<<endl<<s;

	//check if all advanced stuff is here at least
	if(!checkGLRequirements())
	{
		qCritical()<<"A required OpenGL feature are missing in the current context, can not proceed";
		//error has been set
		return false;
	}

	//errors are set in respective functions
	if(!initShaders())
		return false;
	if(!initBuffers())
		return false;
	if(!initTextures())
		return false;

	qDebug()<<"Renderer init in "<<d.timer.restart()<<"ms";

	//all ok
	return true;
}

QOpenGLShaderProgram* VolumeRenderer::createComputeShader(const QString& cShaderName)
{
	const QString filePattern(":/shaders/%1.%2");

	qDebug()<<"Create compute shader: "<<cShaderName;

	QOpenGLShaderProgram* shader = new QOpenGLShaderProgram(this);
	QOpenGLShader* cShader = new QOpenGLShader(QOpenGLShader::Compute,shader);

	QString shaderFile = filePattern.arg(cShaderName,"compute");
	bool ok= cShader->compileSourceFile(shaderFile);
	QString log = cShader->log().trimmed();
	if(!ok)
	{
		//qt shader compiler only creates logs when the compilation failed, warnings can not be acquired
		qCritical()<<"Vertex shader compilation error:"<<endl<<log;
		m_errString = tr("Vertex shader %1 compilation error:\n%2").arg(shaderFile,log);
		delete shader;
		return NULL;
	}
	shader->addShader(cShader);

	ok = shader->link();
	log = shader->log().trimmed();
	//linking, in contrast, can also provide us with warnings
	if(!ok)
	{
		//qt shader compiler only creates logs when the compilation failed, warnings can not be acquired
		qCritical()<<"Shader linking errors:"<<endl<<log;
		m_errString = tr("Shader linking error:\n%1").arg(log);
		delete shader;
		return nullptr;
	}
	else if(!log.isEmpty())
	{
		qWarning()<<"Shader linking warnings:"<<endl<<log;
	}

	if(ok)
		qDebug()<<"Program creation successful";

	return shader;
}

QOpenGLShaderProgram* VolumeRenderer::createShader(const QString& vShaderName, const QString& fShaderName, const QString& gShaderName)
{
	const QString filePattern(":/shaders/%1.%2");
	bool useGshader = !gShaderName.isEmpty();

	if(useGshader)
		qDebug()<<"Create Program: VShader "<<vShaderName<<", GShader "<<gShaderName<<", FShader "<<fShaderName;	
	else
		qDebug()<<"Create Program: VShader "<<vShaderName<<", FShader "<<fShaderName;	

	QOpenGLShaderProgram* shader = new QOpenGLShaderProgram(this);
	QOpenGLShader* vShader = new QOpenGLShader(QOpenGLShader::Vertex,shader);
	QOpenGLShader* gShader = useGshader? new QOpenGLShader(QOpenGLShader::Geometry,shader) : nullptr;
	QOpenGLShader* fShader = new QOpenGLShader(QOpenGLShader::Fragment,shader);


	QString shaderFile = filePattern.arg(vShaderName,"vert");
	bool ok= vShader->compileSourceFile(shaderFile);
	QString log = vShader->log().trimmed();
	if(!ok)
	{
		//qt shader compiler only creates logs when the compilation failed, warnings can not be acquired
		qCritical()<<"Vertex shader compilation error:"<<endl<<log;
		m_errString = tr("Vertex shader %1 compilation error:\n%2").arg(shaderFile,log);
		delete shader;
		return NULL;
	}
	shader->addShader(vShader);

	if(useGshader)
	{
		shaderFile = filePattern.arg(gShaderName,"geom");
		ok = gShader->compileSourceFile(shaderFile);
		log = gShader->log().trimmed();
		if(!ok)
		{
			//qt shader compiler only creates logs when the compilation failed, warnings can not be acquired
			qCritical()<<"Geometry shader compilation error:"<<endl<<log;
			m_errString = tr("Geometry shader %1 compilation error:\n%2").arg(shaderFile,log);
			delete shader;
			return nullptr;
		}
		shader->addShader(gShader);
	}

	shaderFile = filePattern.arg(fShaderName,"frag");
	ok = fShader->compileSourceFile(shaderFile);
	log = fShader->log().trimmed();
	if(!ok)
	{
		//qt shader compiler only creates logs when the compilation failed, warnings can not be acquired
		qCritical()<<"Fragment shader compilation error:"<<endl<<log;
		m_errString = tr("Fragment shader %1 compilation error:\n%2").arg(shaderFile,log);
		delete shader;
		return nullptr;
	}
	shader->addShader(fShader);

	//add typical frag color name
	int shaderId = shader->programId();
	glBindFragDataLocation(shaderId,0,FRAGDATA_LOCATION);

	ok = shader->link();
	log = shader->log().trimmed();
	//linking, in contrast, can also provide us with warnings
	if(!ok)
	{
		//qt shader compiler only creates logs when the compilation failed, warnings can not be acquired
		qCritical()<<"Shader linking errors:"<<endl<<log;
		m_errString = tr("Shader linking error:\n%1").arg(log);
		delete shader;
		return nullptr;
	}
	else if(!log.isEmpty())
	{
		qWarning()<<"Shader linking warnings:"<<endl<<log;
	}

	if(ok)
		qDebug()<<"Program creation successful";

	return shader;
}

bool VolumeRenderer::initShaders()
{
	qDebug()<<"Initializing shaders";

	if((d.shaders.transform = createShader("transform","transform"))==nullptr)
		return false;
	if((d.shaders.color = createShader("color","color")) == nullptr)
		return false;
	if((d.shaders.bBox = createShader("bbox","bbox","bbox"))==nullptr)
		return false;
	if((d.shaders.slice = createShader("slice","slice"))==nullptr)
		return false;
	if((d.shaders.blur = createShader("fsquad","blur"))==nullptr)
		return false;
	if((d.shaders.merge = createShader("fsquad","merge"))==nullptr)
		return false;
	if((d.shaders.gaussian = createComputeShader("gaussian"))==nullptr)
		return false;
	if((d.shaders.gradient = createComputeShader("gradient"))==nullptr)
		return false;

	return true;
}

bool VolumeRenderer::initBuffers()
{
	//macros for error handling
#define VAO_CREATE(vao) if(!vao->create()){\
	m_errString = tr("Could not create vertex array object.");\
	delete vao; \
	return false;}

#define BUFFER_CREATE(buf) if(!buf->create()){\
	m_errString = tr("Could not create buffer object.");\
	delete buf; \
	return false;}

	/// *** BOX 
	//some modern c++ magic :)
	auto vao = d.vaos.box =  new QOpenGLVertexArrayObject(this);
	VAO_CREATE(vao);
	vao->bind();

	auto vBuffer = d.buffers.vBuffer = new QOpenGLBuffer(QOpenGLBuffer::Type::VertexBuffer);
	BUFFER_CREATE(vBuffer);
	vBuffer->bind();

	auto iBuffer = d.buffers.iBuffer = new QOpenGLBuffer(QOpenGLBuffer::Type::IndexBuffer);
	BUFFER_CREATE(iBuffer);
	iBuffer->bind();

	//just for sanity, QVectors are floats
	Q_ASSERT(sizeof(QVector3D) == 3*sizeof(GLfloat));

	auto vtx = d.bBox.getVertices();

	//dem hacks
	QMatrix4x4 mat;
	mat.translate(0.5f,0.5f,0.5f);
	auto cols =  d.bBox.transform(mat);

	//squish the pos and colors together in a buffer
	QByteArray vData((const char*)vtx,8 * sizeof(QVector3D));
	vData.append((const char*)cols.constData(),8*sizeof(QVector4D)); //4. component will be ignored

	vBuffer->allocate(vData.constData(),vData.size());	

	// put triangle and line indices in the same buffer
	QByteArray b;
	size_t triSize = sizeof(Cube::triangleIndices), lineSize = sizeof(Cube::lineIndices);
	b.reserve(triSize + lineSize);
	d.buffers.triangleIndexOffset = 0;
	d.buffers.lineIndexOffset = triSize;
	b.append((const char*)Cube::triangleIndices,triSize).append((const char*)Cube::lineIndices,lineSize);

	iBuffer->allocate(b.constData(),b.size());	

	//config shader
	GLuint pos = ATTRPOS_VERTEX;
	glEnableVertexAttribArray(pos);
	glVertexAttribPointer(pos,3,GL_FLOAT,GL_FALSE,0, 0);
	pos = ATTRPOS_COLOR;
	glEnableVertexAttribArray(pos);
	glVertexAttribPointer(pos,3,GL_FLOAT,GL_FALSE,sizeof(QVector4D),(GLvoid*)(8*sizeof(QVector3D)));

	vao->release();
	vBuffer->release();
	iBuffer->release();

	/// *** LINE
	vao = d.vaos.line = new QOpenGLVertexArrayObject(this);
	VAO_CREATE(vao);
	vao->bind();

	vBuffer = d.buffers.lineBuffer = new QOpenGLBuffer(QOpenGLBuffer::Type::VertexBuffer);
	BUFFER_CREATE(vBuffer);
	vBuffer->bind();

	vBuffer->setUsagePattern(QOpenGLBuffer::StreamDraw);
	vBuffer->allocate(2 * sizeof(QVector3D));

	pos = ATTRPOS_VERTEX;
	glEnableVertexAttribArray(pos);
	glVertexAttribPointer(pos,3,GL_FLOAT,GL_FALSE,0,0);

	vao->release();
	vBuffer->release();

	/// *** SLICES
	vao = d.vaos.slice = new QOpenGLVertexArrayObject(this);
	VAO_CREATE(vao);
	vao->bind();

	//slice has no ibuffers, will be drawn with TRIANGLE_FAN
	vBuffer = d.buffers.sliceVBuffer = new QOpenGLBuffer(QOpenGLBuffer::Type::VertexBuffer);
	BUFFER_CREATE(vBuffer);
	vBuffer->bind();

	//we are streaming all vertices on the fly
	vBuffer->setUsagePattern(QOpenGLBuffer::StreamDraw);

	//the maximal number of intersection points of a plane with a cube is 6, + 1 centroid vertex, +1 repeated vertex
	//preallocate that amount of data for SLICEBUFFER_SLICECOUNT slices
	vBuffer->allocate(SLICEBUFFER_STRIDE * SLICEBUFFER_SLICECOUNT * sizeof(QVector3D));

	pos = ATTRPOS_VERTEX;
	glEnableVertexAttribArray(pos);
	glVertexAttribPointer(pos,3,GL_FLOAT,GL_FALSE,0,0);

	vao->release();
	vBuffer->release();

	return true;
}

bool VolumeRenderer::initTextures()
{
	d.textures.volume = new QOpenGLTexture(QOpenGLTexture::Target::Target3D);
	return true;
}

void VolumeRenderer::updateFBOs()
{
	//delete old fbos
	if(d.fbos.slice)
		delete d.fbos.slice;
	if(d.fbos.eye[0])
		delete d.fbos.eye[0];
	if(d.fbos.eye[1])
		delete d.fbos.eye[1];
	if(d.fbos.intermediate[0])
		delete d.fbos.intermediate[0];
	if(d.fbos.intermediate[1])
		delete d.fbos.intermediate[1];

	//use 16 bit float textures for some added precision
	const GLenum format = GL_RGBA16F;

	//create new fbos
	glActiveTexture(GL_TEXTURE0 + TEXUNIT_SLICE);
	d.fbos.slice = new QOpenGLFramebufferObject(d.winWidth,d.winHeight,QOpenGLFramebufferObject::Attachment::NoAttachment,GL_TEXTURE_2D,format);
	glBindTexture(GL_TEXTURE_2D,d.fbos.slice->texture());
	//default filtering seems to be NEAREST, which is ok for slice texture (1:1 pixel-to-pixel blend is performed)

	glActiveTexture(GL_TEXTURE0 + TEXUNIT_PREVIOUS);
	d.fbos.eye[0] = new QOpenGLFramebufferObject(d.winWidth,d.winHeight,QOpenGLFramebufferObject::Attachment::NoAttachment,GL_TEXTURE_2D,format);
	glBindTexture(GL_TEXTURE_2D,d.fbos.eye[0]->texture());
	// the intermediate/eye buffers should have LINEAR filtering for the blurring to work properly
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

	d.fbos.eye[1] = new QOpenGLFramebufferObject(d.winWidth,d.winHeight,QOpenGLFramebufferObject::Attachment::NoAttachment,GL_TEXTURE_2D,format);
	glBindTexture(GL_TEXTURE_2D,d.fbos.eye[1]->texture());
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

	d.fbos.intermediate[0] = new QOpenGLFramebufferObject(d.winWidth,d.winHeight,QOpenGLFramebufferObject::Attachment::NoAttachment,GL_TEXTURE_2D,format);
	glBindTexture(GL_TEXTURE_2D,d.fbos.intermediate[0]->texture());
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

	d.fbos.intermediate[1] = new QOpenGLFramebufferObject(d.winWidth,d.winHeight,QOpenGLFramebufferObject::Attachment::NoAttachment,GL_TEXTURE_2D,format);
	glBindTexture(GL_TEXTURE_2D,d.fbos.intermediate[1]->texture());
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);


	qDebug()<<"Framebuffer objects updated";
}