/// 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.

#include "OpenglDriver.h"

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

#include "Exception.h"
#include "UnicodeString.h"
#include <assert.h>

using namespace TestFrameWork;

OpenglDriver::OpenglDriver(OpenglVersion version, HDC canvasContext, 
						   size_t width, size_t height,
						   int depth, int samples)	:	m_version(version), m_canvasContext(canvasContext),
														m_width(width), m_height(height),
														m_openglContext(canvasContext, depth, samples)
{
	InitializeGlew();

	InitializeGlut();

	CheckOpenglVersion(version);

	CheckMinRequirements(version);
	TestFrameWork::OnGLErrorThrowException();

	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	glEnable(GL_CULL_FACE);
	glEnable(GL_DEPTH_TEST);
	glDepthFunc(GL_LESS);
	glClearColor(0.5f, 0.5f, 0.7f, 0.0f);
}

OpenglDriver::~OpenglDriver(void)
{
}

void OpenglDriver::BeginFrame(void)
{
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	TestFrameWork::OnGLErrorThrowException();
}

void OpenglDriver::EndFrame(void)
{  
	if (SwapBuffers(m_canvasContext) != TRUE)
	{    
		throw Exception();
	}
}

size_t OpenglDriver::CreateVertexBuffer(Vertices vertices, size_t verticesCount)
{
	if (!verticesCount)
	{
		throw Exception(L"Invalid parameter: verticesCount == 0 in OpenglDriver::CreateVertexBuffer");
	}

	if (!vertices)
	{
		throw Exception(L"Invalid parameter: !vertices in OpenglDriver::CreateVertexBuffer");
	}

	OpenglVertexBufferPtr vb(new OpenglVertexBuffer(vertices, verticesCount));

	m_vertexBuffers.push_back(vb);

	return m_vertexBuffers.size() - 1;
}

size_t OpenglDriver::CreateIndexBuffer(Indices32 indices, size_t indicesCount)
{
	if (!indicesCount)
	{
		throw Exception(L"Invalid parameter: indicesCount == 0 in OpenglDriver::CreateVertexBuffer");
	}

	if (!indices)
	{
		throw Exception(L"Invalid parameter: !indices in OpenglDriver::CreateVertexBuffer");
	}

	OpenglIndexBuffer32Ptr ib(new OpenglIndexBuffer32(indices, indicesCount));

	TestFrameWork::OnGLErrorThrowException();

	m_indexBuffers.push_back(ib);

	return m_indexBuffers.size() - 1;
}

OpenglProgramWeakPtr OpenglDriver::CreateProgram(const std::vector<ShaderFileDesc>& shaders)
{
	if (!shaders.size())
	{
		throw Exception(L"Invalid parameter: !shaders.size() OpenglDriver::CreateProgram");
	}

	OpenglProgramPtr program(new OpenglProgram(shaders));
		
	TestFrameWork::OnGLErrorThrowException();

	m_programs.push_back(program);

	return OpenglProgramWeakPtr(program);
}

OpenglTextureWeakPtr OpenglDriver::CreateTexture(TextureData data, OpenglTextureFormat format, 
											     size_t width, OpenglTextureFilter filter, 
											     OpenglTextureEdgeValue edge)
{
	OpenglTexturePtr texture(new OpenglTexture(data, format, width, filter, edge));
	m_textures.push_back(texture);

	return OpenglTextureWeakPtr(texture);
}

OpenglTextureWeakPtr OpenglDriver::CreateTexture(TextureData data, OpenglTextureFormat format, 
											     size_t width, size_t height, OpenglTextureFilter filter, 
												 OpenglTextureEdgeValue edge)
{
	OpenglTexturePtr texture(new OpenglTexture(data, format, width, height, filter, edge));
	m_textures.push_back(texture);

	return OpenglTextureWeakPtr(texture);
}

OpenglTextureWeakPtr OpenglDriver::CreateTexture(TextureData data, OpenglTextureFormat format, 
											     size_t width, size_t height, size_t depth, 
											     OpenglTextureFilter filter, OpenglTextureEdgeValue edge)
{
	OpenglTexturePtr texture(new OpenglTexture(data, format, width, height, depth, filter, edge));
	m_textures.push_back(texture);

	return OpenglTextureWeakPtr(texture);
}

void OpenglDriver::SetProgram(OpenglProgramWeakPtr program)
{
	m_currentProgram = program;
}

void OpenglDriver::SetTexture(OpenglTextureWeakPtr texture, int textureUnit)
{
	m_textureUnits[texture] = textureUnit;
}

void OpenglDriver::DrawIndexedPrimitive(OpenglPrimitive primitive, size_t vertexBufferId, 
										size_t indexBufferId, size_t primitivesCount)
{
	if (vertexBufferId < 0 || vertexBufferId >= m_vertexBuffers.size())
	{
		throw Exception(L"Invalid Parameter: vertexBufferId < 0 || vertexBufferId > m_vertexBuffers.size() in OpenglDriver::DrawIndexedPrimitive");
	}

	if (indexBufferId < 0 || indexBufferId >= m_indexBuffers.size())
	{
		throw Exception(L"Invalid Parameter: indexBufferId < 0 || indexBufferId > m_indexBuffers.size() in OpenglDriver::DrawIndexedPrimitive");
	}

	// Set program
	SetCurrentProgram();

	// Set textures
	SetCurrentTextures();
	
	// Set buffers
	OpenglVertexBufferPtr vertexBuffer(m_vertexBuffers[vertexBufferId]);
	OpenglIndexBuffer32Ptr indexBuffer(m_indexBuffers[indexBufferId]);
	SetBuffers(vertexBuffer, indexBuffer);

	/// TODO:
	/// Fixed index type to 32bits
	GLenum mode = Primitive2GlMode(primitive);
	if (primitivesCount == 1)
	{
		glDrawElements(mode, static_cast<GLsizei>(indexBuffer->GetIndicesCount()), GL_UNSIGNED_INT, 0);
		TestFrameWork::OnGLErrorThrowException();
	}
	else
	{
		glDrawElementsInstanced(mode, static_cast<GLsizei>(indexBuffer->GetIndicesCount()), GL_UNSIGNED_INT, 0, static_cast<GLsizei>(primitivesCount));
		TestFrameWork::OnGLErrorThrowException();
	}
}

void OpenglDriver::GetMaximumTextureSize(OpenglTextureFormat format, size_t& width, size_t& height)
{
	GLint widthResult = 0;
	GLint internalFormat = OpenglTexture::Format2InternalFormat(format);
	GLenum pixelFormat = OpenglTexture::Format2Gl(format);
	GLsizei proxySize = 8192;
	while (!widthResult) 
	{
		glTexImage2D(GL_PROXY_TEXTURE_2D, 0, internalFormat, proxySize, proxySize, 0, pixelFormat, GL_FLOAT, 0);

		glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &widthResult);

		proxySize /= 2;
	}

	width = proxySize;
	height = proxySize;
}

void OpenglDriver::Draw2DString(const std::string& s, size_t x, size_t 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(static_cast<GLint>(x), static_cast<GLint>(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 OpenglDriver::InitializeGlew(void)
{
	GLenum err = glewInit();
	if (GLEW_OK != err)
	{
		throw Exception(ConvertUTF8ToUTF16(reinterpret_cast<const char*>(glewGetErrorString(err))));
	}
}

void OpenglDriver::InitializeGlut(void)
{
	int argc = 0;
	glutInit(&argc, 0);
}

void OpenglDriver::CheckOpenglVersion(OpenglVersion version) const
{
	switch (version)
	{		
		case OpenglVersion_2_0:
		if (!GLEW_VERSION_2_0)
		{
			throw Exception(L"Opengl 2.0 required");
		}
		break;

		case OpenglVersion_2_1:
			if (!GLEW_VERSION_2_1)
			{
				throw Exception(L"Opengl 2.1 required");
			}
			break;

		case OpenglVersion_3_0:
			if (!GLEW_VERSION_3_0)
			{
				throw Exception(L"Opengl 3.0 required");
			}
			break;

		case OpenglVersion_3_1:
			if (!GLEW_VERSION_3_1)
			{
				throw Exception(L"Opengl 3.1 required");
			}
			break;		
		
		case OpenglVersion_3_2:
			if (!GLEW_VERSION_3_2)
			{
				throw Exception(L"Opengl 3.2 required");
			}
			break;

		default:
			assert(false);
			break;
	}
}

void OpenglDriver::CheckMinRequirements(OpenglVersion /*version*/) const
{
	if (!GLEW_EXT_gpu_shader4)
	{
		throw Exception(L"GLEW_EXT_gpu_shader4 not supported");
	}

	if (!GLEW_ARB_draw_instanced)
	{
		throw Exception(L"ARB_draw_instanced not supported");
	}

	if (!GLEW_ARB_texture_float)
	{
		throw Exception(L"ARB_texture_float not supported");
	}
}

#ifdef _DEBUG
GLenum OpenglDriver::Primitive2GlMode(OpenglPrimitive primitive) const
#else
GLenum OpenglDriver::Primitive2GlMode(OpenglPrimitive) const
#endif
{
	assert(primitive == Triangles);

	return GL_TRIANGLES;
}

void OpenglDriver::SetCurrentProgram(void) const
{
	OpenglProgramPtr program(m_currentProgram.lock());

	glUseProgram(program->GetOpenglId());
	TestFrameWork::OnGLErrorThrowException();
}

void OpenglDriver::SetCurrentTextures(void) const
{
	std::map<OpenglTextureWeakPtr, int>::const_iterator it = m_textureUnits.begin();
	std::map<OpenglTextureWeakPtr, int>::const_iterator itEnd = m_textureUnits.end();

	while (it != itEnd)
	{
		OpenglTextureWeakPtr textureWeak(it->first);
		OpenglTexturePtr texture(textureWeak.lock());
		
		glActiveTexture(GL_TEXTURE0 + it->second);
		TestFrameWork::OnGLErrorThrowException();
		glBindTexture(texture->GetTarget(), texture->GetOpenglId());
		TestFrameWork::OnGLErrorThrowException();

		++it;
	}
}

void OpenglDriver::SetBuffers(OpenglVertexBufferPtr vertexBuffer, OpenglIndexBuffer32Ptr indexBuffer) const
{
	/// Bind Vertex Buffer
	glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer->GetOpenglId());
	TestFrameWork::OnGLErrorThrowException();

	/// TODO:
	/// Fixed vertex format to Vertex(position and normal)
	glEnableClientState(GL_VERTEX_ARRAY);
	TestFrameWork::OnGLErrorThrowException();
	glVertexPointer(3, GL_FLOAT, sizeof(Vertex), (GLubyte*)0);
	TestFrameWork::OnGLErrorThrowException();

	glEnableClientState(GL_NORMAL_ARRAY);
	TestFrameWork::OnGLErrorThrowException();
	glNormalPointer(GL_FLOAT, sizeof(Vertex), (GLubyte*)0 + 3 * sizeof(float));
	TestFrameWork::OnGLErrorThrowException();

	/// Bind index buffer
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer->GetOpenglId());
	TestFrameWork::OnGLErrorThrowException();
}