#include "..\Header\OpenGlViewer.h"
#include <cmath>
#include <iostream>
#include <QMessageBox>


/////////////////////////////////////////////////////////////////////////
// Defined values

//opengl
#define DEFAULT_MAJOR_VERSION		4
#define DEFAULT_MINOR_VERSION		5
#define DEAFAULT_BACKGROUND			0.5000f, 0.8000f, 1.0000f, 0.0000f

//piplines
#define VERTEX_INDEX_XYZ		0
#define VERTEX_INDEX_UV			1

#define VERTEX_COMPONENTS_XYZ	3
#define VERTEX_COMPONENTS_UV	2

#define VERTEX_SIZE_XYZ			(sizeof(float) * VERTEX_COMPONENTS_XYZ)
#define VERTEX_SIZE_UV			(sizeof(float) * VERTEX_COMPONENTS_UV)

#define VERTEX_OFFSET_XYZ		0
#define VERTEX_OFFSET_UV		(VERTEX_SIZE_XYZ)


/////////////////////////////////////////////////////////////////////////
// public constructor/destructor

OpenGlViewer::OpenGlViewer(QWidget *parent)
	: QOpenGLWidget(parent)
{
	QSurfaceFormat format;
	format.setRenderableType(QSurfaceFormat::OpenGL);
	format.setSamples(4);
	format.setProfile(QSurfaceFormat::CoreProfile);
	format.setVersion(DEFAULT_MAJOR_VERSION, DEFAULT_MINOR_VERSION);
	setFormat(format);

	//TODO: mouse, move, key, drag/drop, scroll, resize
}

OpenGlViewer::~OpenGlViewer()
{
	glDeleteTextures(1, &m_oglTexture);
	glDeleteBuffers(1, &m_vertexBuffer);
	m_vertexArray.destroy();
	delete m_program;

	deleteData();
}


/////////////////////////////////////////////////////////////////////////
// private functions

void OpenGlViewer::initializeGL()
{
	initializeOpenGLFunctions();
	printContextInformation();

	// set Background
	glClearColor(DEAFAULT_BACKGROUND);

	//TODO: z-order?

	// draw vertices only from one side
	glEnable(GL_CULL_FACE);

	// Create texture
	glGenTextures(1, &m_oglTexture);
	glBindTexture(GL_TEXTURE_2D, m_oglTexture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

	// Create Shader
	m_program = new QOpenGLShaderProgram();
	m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/simple.vert");
	m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/simple.frag");
	m_program->link();
	m_program->bind();

	// get Uniform location
	//TODO: faster to give everything to shader and calculate there?
	m_uniformMVP = m_program->uniformLocation("MVP");

	// Create Vertex Buffer
	glGenBuffers(1, &m_vertexBuffer);
	glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer);

	// Create Vertex Array Object
	m_vertexArray.create();
	m_vertexArray.bind();
	m_program->enableAttributeArray(0);
	m_program->enableAttributeArray(1);
	m_program->setAttributeBuffer(VERTEX_INDEX_XYZ, GL_FLOAT, VERTEX_OFFSET_XYZ, VERTEX_COMPONENTS_XYZ, sizeof(Vertex));
	m_program->setAttributeBuffer(VERTEX_INDEX_UV, GL_FLOAT, VERTEX_OFFSET_UV, VERTEX_COMPONENTS_UV, sizeof(Vertex));

	// unbind everything
	m_vertexArray.release();
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	m_program->release();

}

void OpenGlViewer::paintGL()
{
	//TODO: paint here
	glClear(GL_COLOR_BUFFER_BIT);

	m_program->bind();

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, m_oglTexture);
	m_program->setUniformValue("textureSampler", 0);
	

	if (m_vModels != nullptr)
	{
		int tmp_offset(0);

		for (unsigned int modelIndex = 0; modelIndex < m_vModels->size(); modelIndex++)
		{
			// skip null, bones, shadowMesh, hidden things
			if (m_vModels->at(modelIndex)->type == null ||
				m_vModels->at(modelIndex)->type == bone ||
				m_vModels->at(modelIndex)->type == shadowMesh ||
				m_vModels->at(modelIndex)->renderFlags == 1)
				continue;

			for (auto& segmentIterator : m_vModels->at(modelIndex)->segmList)
			{
				// set the texture
				std::uint32_t tmp_textureIndex = segmentIterator->textureIndex >= m_vTextures->size() ? m_vTextures->size() - 1 : segmentIterator->textureIndex;

				glTexImage2D(
					GL_TEXTURE_2D,
					0,
					m_vTextures->at(tmp_textureIndex)->alpha ? GL_RGBA : GL_RGB,
					m_vTextures->at(tmp_textureIndex)->width,
					m_vTextures->at(tmp_textureIndex)->height,
					0,
					m_vTextures->at(tmp_textureIndex)->alpha ? GL_BGRA : GL_BGR,
					GL_UNSIGNED_BYTE,
					m_vTextures->at(tmp_textureIndex)->data->data()
				);

				glGenerateMipmap(GL_TEXTURE_2D);

				// give the MVP to the shader
				m_program->setUniformValue(m_uniformMVP, getMVPMatrix(modelIndex));

				// calculate the number of vertex
				unsigned int tmp_vertexCount(0);
				for (auto&it : segmentIterator->polyIndices)
					tmp_vertexCount += (it.size() - 2) * 3;

				glDrawArrays(GL_TRIANGLES, tmp_offset, tmp_vertexCount);

				// increase the offset
				tmp_offset += tmp_vertexCount;
			}
		}
	}

	m_program->release();

	this->frameSwapped();
}

void OpenGlViewer::printContextInformation()
{
	QString glType;
	QString glVersion;
	QString glProfile;

	glType = (context()->isOpenGLES()) ? "OpenGL ES" : "OpenGL";
	glVersion = reinterpret_cast<const char*>(glGetString(GL_VERSION));

#define CASE(c) case QSurfaceFormat::c: glProfile = #c; break
	switch (format().profile())
	{
		CASE(NoProfile);
		CASE(CoreProfile);
		CASE(CompatibilityProfile);
	}
#undef CASE

	std::cout << glType.toStdString() << " - " << glVersion.toStdString() << " (" << glProfile.toStdString() << ")";
}

QMatrix4x4 OpenGlViewer::getModelMatrix(unsigned int index) const
{
	QMatrix4x4 tmp_parent;

	for (unsigned int loop = 0; loop < m_vModels->size(); loop++)
	{
		if (!strcmp(m_vModels->at(index)->parent.c_str(), m_vModels->at(loop)->name.c_str()))
		{
			tmp_parent = getModelMatrix(loop);
			break;
		}
	}

	return tmp_parent * m_vModels->at(index)->m4x4Translation;
}

QMatrix4x4 OpenGlViewer::getMVPMatrix(unsigned int index) const
{
	QMatrix4x4 tmp_mvp;
	
	// projection
	tmp_mvp.perspective(m_fFOV, float(QWidget::width()) / float(QWidget::height()), m_fMinView, m_fMaxView);
	
	// view
	tmp_mvp.lookAt(QVector3D(m_fTranX, m_fTranY, m_fTranZ), QVector3D(m_fTranX, m_fTranY, m_fTranZ - 1), QVector3D(0, 0, 1));
	
	// user controlled rotation
	tmp_mvp.rotate(m_fRotX, QVector3D(1, 0, 0));
	tmp_mvp.rotate(m_fRotY, QVector3D(0, 1, 0));
	tmp_mvp.rotate(m_fRotZ, QVector3D(0, 0, 1));

	//scale to 1
	float maxExtent = std::max(std::max(m_sceneBoundings.extents[0], m_sceneBoundings.extents[1]), m_sceneBoundings.extents[2]);
	tmp_mvp.scale(1 / maxExtent); 

	// move to center
	tmp_mvp.translate(-m_sceneBoundings.center[0], -m_sceneBoundings.center[1], -m_sceneBoundings.center[2]);

	return tmp_mvp * getModelMatrix(index);
}

void OpenGlViewer::deleteData()
{
	if (m_vModels != nullptr)
	{
		while (!m_vModels->empty())
		{
			// remove the last Model
			Model* modelVectorElement = m_vModels->back();
			m_vModels->pop_back();

			while (!modelVectorElement->segmList.empty())
			{
				// remove the last Segment
				Segment* segmentVectorElement = modelVectorElement->segmList.back();
				modelVectorElement->segmList.pop_back();

				// delete data from Segment
				delete[] segmentVectorElement->uv;
				delete[] segmentVectorElement->vertex;

				while (!segmentVectorElement->polyIndices.empty())
				{
					// clear the poly vector and remove it from the list
					segmentVectorElement->polyIndices.back().clear();
					segmentVectorElement->polyIndices.pop_back();
				}

				// delete the actual Segment
				delete segmentVectorElement;
			}

			// delete the actual Model
			delete modelVectorElement;
		}

		// delete the Model's Vector
		delete m_vModels;
	}

	if (m_vTextures != nullptr)
	{
		while (!m_vTextures->empty())
		{
			// remove the last texture
			TextureData* cursor = m_vTextures->back();
			m_vTextures->pop_back();

			cursor->data->clear();
			delete cursor->data;

			//delete image
			delete cursor;
		}
		// delete the Textrue's Vector
		delete m_vTextures;
	}
}


/////////////////////////////////////////////////////////////////////////
// public functions

void OpenGlViewer::setData(std::vector<Model*>* models, std::vector<TextureData*>* textures, BoundingBox bbox)
{
	// new Data, so clean up the old things
	deleteData();

	// save the variables
	m_vModels = models;
	m_vTextures = textures;
	m_sceneBoundings = bbox;

	// collect vertex data of all models
	std::vector<Vertex> tmp_bufferData;

	for (auto& modIt : *m_vModels)		// for every model chunk
	{
		for (auto& segIt : modIt->segmList)		// for every cluster
		{
			for (auto& mshIt : segIt->polyIndices)		// for every polygon
			{
				if (mshIt.size() >= 3)		// multipoly
				{
					// for every triangle of the multi polygon
					for (unsigned int tri = 0; tri < mshIt.size() - 2; tri++)
					{
						// for every edge of the triangle
						for (int triEdge = 0; triEdge < 3; triEdge++)
						{
							Vertex tempVertex;
							// every edge has 3 coordinates
							for (int j = 0; j < 3; j++)
								tempVertex.position[j] = (GLfloat)segIt->vertex[mshIt[tri + triEdge - ((tri % 2) * (triEdge - 1) * 2)] * 3 + j];

							// and 2 UV
							if (segIt->uv == NULL)
							{
								tempVertex.uv[0] = 1.0;
								tempVertex.uv[1] = 1.0;
							}
							else
							{
								for (int j = 0; j < 2; j++)
									tempVertex.uv[j] = (GLfloat)segIt->uv[mshIt[tri + triEdge - ((tri % 2) * (triEdge - 1) * 2)] * 2 + j];
							}
							tmp_bufferData.push_back(tempVertex);
						}
					}
				}
				else	// this shouldn't happen (polygon with less then 3 vertex)
				{
					deleteData();

					QMessageBox msg(this);
					msg.addButton(QMessageBox::Ok);
					msg.setText("You have polygons with less then 3 vertices!");
					msg.setIcon(QMessageBox::Critical);
					msg.setWindowTitle("Open File Error");
					msg.exec();
					return;
				}
			}
		}
	}

	glBindBuffer(GL_ARRAY_BUFFER, m_vertexBuffer);
	glBufferData(
		GL_ARRAY_BUFFER,
		sizeof(Vertex) * tmp_bufferData.size(),
		tmp_bufferData.data(),
		GL_STATIC_DRAW
	);
	glBindBuffer(GL_ARRAY_BUFFER, 0);

	tmp_bufferData.clear();
}
