#include "graphics/shader.h"

#include "utility/logger.h"
#include <cassert>

// Load a generic fallback shader
Shader::Shader()
{
	std::string vertexSource = {
		// generic vertex shader program
		"#version 330 core\n"
		"layout (location = 0) in vec3 aPos;\n"
		"layout (location = 1) in vec2 aTexCoord;\n"
		"out vec2 texCoord;\n"
		"void main()\n"
		"{\n"
		"	texCoord = aTexCoord;\n"
		"	gl_Position = vec4(aPos, 1.0);\n"
		"}\0"
	};
	std::string fragmentSource = {
		// generic fragment shader program
		"#version 330 core\n"
		"in vec2 texCoord;\n"
		"out vec4 FragColor;\n"
		"uniform sampler2D sprite;\n"
		"void main()\n"
		"{\n"
		"	FragColor = texture(sprite, texCoord);\n"
		"}\0"
	};
	compileAndLink(vertexSource.c_str(), fragmentSource.c_str());
	valid = true;
}

Shader::Shader(const char* vertPath, const char* fragPath) : vertexPath(vertPath), fragmentPath(fragPath)
{
	std::string vertexSource;
	std::string fragmentSource;
	std::ifstream vertexStream;
	std::ifstream fragmentStream;

	vertexStream.exceptions(std::ifstream::badbit | std::ifstream::failbit);
	fragmentStream.exceptions(std::ifstream::badbit | std::ifstream::failbit);
	try
	{
		vertexStream.open(vertexPath);
		fragmentStream.open(fragmentPath);

		std::stringstream vStringStream, fStringStream;

		vStringStream << vertexStream.rdbuf();
		fStringStream << fragmentStream.rdbuf();

		vertexSource = vStringStream.str();
		fragmentSource = fStringStream.str();

		vertexStream.close();
		fragmentStream.close();
	}
	catch(std::exception e)
	{
		LOG(ERROR, "failed to open shader files '{}', '{}' error: {}", vertexPath, fragmentPath, e.what());
		assert(1 == 0); // force crash
	}
	const char* vSource = vertexSource.c_str();
	const char* fSource = fragmentSource.c_str();
	if (!compileAndLink(vertexSource.c_str(), fragmentSource.c_str())) {
		LOG(ERROR, "Failed to compile and/or link shader! Check error details for more information.\nUsing fallback shader!", NULL);
		valid = false;
	}
	valid = true;
}

bool Shader::compileAndLink(const char *vSource, const char *fSource)
{
	unsigned int vertexid, fragmentid;
	char infoLog[512];
	int success;

	//compile vertex shader
	vertexid = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexid, 1, &vSource, NULL);
	glCompileShader(vertexid);

	glGetShaderiv(vertexid, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertexid, 512, NULL, infoLog);
		LOG(ERROR, "VERTEX SHADER '{}' COMPILE ERROR\n{}", vertexPath, infoLog);
		return false;
	}

	//compile fragment shader
	fragmentid = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragmentid, 1, &fSource, NULL);
	glCompileShader(fragmentid);

	glGetShaderiv(fragmentid, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(fragmentid, 512, NULL, infoLog);
		LOG(ERROR, "FRAGMENT SHADER '{}' COMPILE ERROR\n{}", fragmentPath, infoLog);
		return false;
	}

	//create and link program with compiled shaders
	ID = glCreateProgram();
	glAttachShader(ID, vertexid);
	glAttachShader(ID, fragmentid);
	glLinkProgram(ID);

	glGetProgramiv(ID, GL_LINK_STATUS, &success);
	if (!success)
	{
		glGetProgramInfoLog(ID, 512, NULL, infoLog);
		LOG(ERROR, "PROGRAM LINKER ERROR\n{}", infoLog);
		return false;
	}

	glDeleteShader(vertexid);
	glDeleteShader(fragmentid);

	glUniformBlockBinding(ID, glGetUniformBlockIndex(ID, "Matrices"), 0);
	return true;
}

void Shader::setFloat(const std::string& name, float value)
{
	glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::setFloatArray(const std::string& name, size_t count, const float* value)
{
	glUniform1fv(glGetUniformLocation(ID, name.c_str()), count, value);
}

void Shader::setIntArray(const std::string& name, size_t count, const int* value)
{
	glUniform1iv(glGetUniformLocation(ID, name.c_str()), count, value);
}

void Shader::setInt(const std::string& name, int value)
{
	glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::setBool(const std::string& name, bool value)
{
	glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::setVec2(const std::string& name, const float* value)
{
	glUniform2fv(glGetUniformLocation(ID, name.c_str()), 1, value);
}

void Shader::setMatrix4f(const std::string& name, const float* value)
{
	GLuint loc = glGetUniformLocation(ID, name.c_str());
	glUniformMatrix4fv(loc, 1, GL_FALSE, value);
}

Shader::~Shader()
{
	glDeleteProgram(ID);
}
