/******************************************************************************
*	File		-	glShader.cpp
*	Author		-	Joey Pollack
*	Date		-	2018/06/26 (y/m/d)
*	Mod Date	-	2021/09/02 (y/m/d)
*	Description	-	Manages a single OpenGL shader program
*
******************************************************************************/

#include "glShader.h"

#include <fstream>
#include <iostream>
#include <sstream>
#include <cstdarg>

#include <glad/gl.h>

#include <utils/logger.h>

namespace lunarium
{

	glShader::glShader()
		: mProgram(0), mbProgramBuilt(false), mpLogger(Logger::GetInstance())
	{
		int glTypes[TYPE_COUNT];
		glTypes[TYPE_VERTEX] = GL_VERTEX_SHADER;
		glTypes[TYPE_GEOMETRY] = GL_GEOMETRY_SHADER;
		glTypes[TYPE_FRAGMENT] = GL_FRAGMENT_SHADER;

		for (int i = 0; i < TYPE_COUNT; i++)
		{
			mShaders[i].ID = 0;
			mShaders[i].Source = "";
			mShaders[i].IsCompiled = false;
			mShaders[i].GLType = glTypes[i];
		}
	}

	bool glShader::IsBuilt() const
	{
		return mbProgramBuilt;
	}


	bool glShader::SetUniformi(const char * uniformName, std::vector<int> values, bool asVector)
	{
		int uniformLocation = glGetUniformLocation(mProgram, uniformName);

		if (asVector)
		{
			int count = ((int)values.size()) / 4;
			glUniform4iv(uniformLocation, count, (int*)values.data());
			return true;
		}

		if (values.size() == 1)
		{
			glUniform1i(uniformLocation, values[0]);
			return true;
		}
		else if (values.size() == 2)
		{
			glUniform2i(uniformLocation, values[0], values[1]);
			return true;
		}
		else if (values.size() == 3)
		{
			glUniform3i(uniformLocation, values[0], values[1], values[2]);
			return true;
		}
		else if (values.size() == 4)
		{
			glUniform4i(uniformLocation, values[0], values[1], values[2], values[3]);
			return true;
		}
		return false;
	}

	bool glShader::SetUniformf(const char * uniformName, std::vector<float> values, bool asVector)
	{
		int uniformLocation = glGetUniformLocation(mProgram, uniformName);

		if (asVector)
		{
			int count = ((int)values.size()) / 4;
			glUniform4fv(uniformLocation, count, (float*)values.data());
			return true;
		}
		
		if (values.size() == 1)
		{
			glUniform1f(uniformLocation, values[0]);
			return true;
		}
		else if (values.size() == 2)
		{
			glUniform2f(uniformLocation, values[0], values[1]);
			return true;
		}
		else if (values.size() == 3)
		{
			glUniform3f(uniformLocation, values[0], values[1], values[2]);
			return true;
		}
		else if (values.size() == 4)
		{
			glUniform4f(uniformLocation, values[0], values[1], values[2], values[3]);
			return true;
		}
		return false;
	}

	bool glShader::SetUniformMatrix(const char * uniformName, int numMats, float * pData)
	{
		int uniformLocation = glGetUniformLocation(mProgram, uniformName);

		if (-1 == uniformLocation)
		{
			Logger::Error(LogCategory::GRAPHICS,
				"Unable to find uniform location. Uniform name: %s", uniformName);
			return false;
		}

		glUniformMatrix4fv(uniformLocation, numMats, GL_FALSE, pData);

		return true;
	}

	void glShader::LogAllUniforms() const
	{
		GLint size; // size of the variable
		GLenum type; // type of the variable (float, vec3 or mat4, etc)

		const GLsizei bufSize = 16; // maximum name length
		GLchar name[bufSize]; // variable name in GLSL
		GLsizei length; // name length
		int count = -1;

		glGetProgramiv(mProgram, GL_ACTIVE_UNIFORMS, &count);
		Logger::Debug(LogCategory::GRAPHICS, "Active Uniforms: %d", count);

		for (int i = 0; i < count; i++)
		{
			glGetActiveUniform(mProgram, (GLuint)i, bufSize, &length, &size, &type, name);

			Logger::Debug(LogCategory::GRAPHICS, "Uniform #%d Type: %u Name: %s", i, type, name);
		}
	}

	bool glShader::SetSource(int sourceType, const char * source, bool loadFromFile)
	{
		if (sourceType < 0 || sourceType >= TYPE_COUNT)
		{
			Logger::Error(LogCategory::GRAPHICS,
				"Can not set shader source (%s) - Invalid Source Type (%d)", source, (int)sourceType);
			return false;
		}

		if (loadFromFile)
		{
			mShaders[sourceType].Source = LoadShaderFromFile(source);
			if ("" == mShaders[sourceType].Source)
				return false;
		}
		else
		{
			mShaders[sourceType].Source = source;
		}


		return true;
	}


	bool glShader::CompileShader(int sourceType)
	{
		if (sourceType < 0 || sourceType >= TYPE_COUNT)
		{
			Logger::Error(LogCategory::GRAPHICS,
				"Can not compile source - Invalid Source Type: %d", (int)sourceType);
			return false;
		}

		if ("" == mShaders[sourceType].Source)
		{
			Logger::Error(LogCategory::GRAPHICS,
				"Can not compile %s shader source - Source is empty", TypeNames[sourceType]);
			return false;
		}

		int  success;
		char infoLog[512];

		// Create shader
		mShaders[sourceType].ID = glCreateShader(mShaders[sourceType].GLType);

		// compile source
		const char* tempSource = mShaders[sourceType].Source.c_str();
		glShaderSource(mShaders[sourceType].ID, 1, &tempSource, NULL);
		glCompileShader(mShaders[sourceType].ID);


		// check for errors
		glGetShaderiv(mShaders[sourceType].ID, GL_COMPILE_STATUS, &success);
		if (!success)
		{
			glGetShaderInfoLog(mShaders[sourceType].ID, 512, NULL, infoLog);
			Logger::Error(LogCategory::GRAPHICS,
				"Error compiling %s shader source: %s", TypeNames[sourceType], infoLog);
			return false;
		}

		mShaders[sourceType].IsCompiled = true;
		return true;
	}

	bool glShader::BuildProgram(bool autoFreeShaders)
	{
		if ("" == mShaders[TYPE_VERTEX].Source || "" == mShaders[TYPE_FRAGMENT].Source)
		{
			Logger::Error(LogCategory::GRAPHICS,
				"Cannot create shader program - vertex and fragment shader must be set!");
			return false;
		}


		mProgram = glCreateProgram();


		for (int i = 0; i < TYPE_COUNT; i++)
		{

			// Compile shaders if needed
			// -------------------------------------------
			if (!mShaders[i].IsCompiled)
			{
				if (mShaders[i].Source == "")
				{
					Logger::Trace(LogCategory::GRAPHICS, 
						"Skipping compilation of %s shader source - source is empty", TypeNames[i]);
				}
				else
				{
					if (!CompileShader(i))
					{
						return false;
					}
				}
				
			}

			// Attach the shader to the program
			// -------------------------------------------
			if (mShaders[i].Source != "")
				glAttachShader(mProgram, mShaders[i].ID);
		}


		// Link and check for errors
		glLinkProgram(mProgram);

		int  success;
		char infoLog[512];

		glGetProgramiv(mProgram, GL_LINK_STATUS, &success);
		if (!success)
		{
			glGetProgramInfoLog(mProgram, 512, NULL, infoLog);
			Logger::Error(LogCategory::GRAPHICS,
				"Failed to link the shader program. Info Log: %s", infoLog);
			return false;
		}

		// Clean up
		if (autoFreeShaders)
		{
			FreeShaders();
		}

		mbProgramBuilt = true;
		return true;
	}


	void glShader::FreeShaders()
	{
		for (int i = 0; i < TYPE_COUNT; i++)
		{
			if (mShaders[i].IsCompiled)
			{
				glDeleteShader(mShaders[i].ID);
				mShaders[i].ID = 0;
				mShaders[i].IsCompiled = false;
			}
		}
	}

	void glShader::MakeActive()
	{
		glUseProgram(mProgram);
	}

	std::string glShader::LoadShaderFromFile(const char * filename)
	{
		std::ifstream ifs(filename);

		if (!ifs.is_open())
		{
			Logger::Error(LogCategory::GRAPHICS,
				"Failed to load shader source from file (file not found): %s", filename);
			return "";
		}

		std::stringstream stream;
		stream << ifs.rdbuf();
		ifs.close();

		return stream.str();
	}
}