#include <opengl-playground/openglshader.hpp>
#include <opengl-playground/globalcanvassettings.hpp>
#include <string.h>
#include <fstream>
#include <iostream>

OpenGlShaderProgram::OpenGlShaderProgram(std::string vertex_file, std::string geometry_file, std::string fragment_file)
{
	this->vertex_file = vertex_file;
	this->geometry_file = geometry_file;
	this->fragment_file = fragment_file;
	this->compiled = false;
}

OpenGlShaderProgram::~OpenGlShaderProgram()
{
	this->unload();
}

std::string load_shader_program_from_file(std::string &file_name)
{
	std::ifstream ifs(file_name);
	std::string file_content((std::istreambuf_iterator<char>(ifs)),
				 (std::istreambuf_iterator<char>()));

	return file_content;
}

int OpenGlShaderProgram::compile()
{
	int ret_code = 0;
	int success;
	char info_log[512];
	unsigned int vertex_shader, geometry_shader, fragment_shader;
	bool vertex_b = false, geo_b = false, frag_b = false;
	std::string src_code;
	const char *c_string;

	if (this->compiled) {
		glDeleteProgram(shader_program);
		this->compiled = false;
	}

	this->shader_program = glCreateProgram();

	if (!vertex_file.empty()) {
		vertex_b = true;

		src_code = load_shader_program_from_file(this->vertex_file);
		c_string = src_code.c_str();

		/* Compile the vertex shader */
		vertex_shader = glCreateShader(GL_VERTEX_SHADER);
		glShaderSource(vertex_shader, 1, &c_string, NULL);
		glCompileShader(vertex_shader);
		glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);

		if (!success) {
			glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
			printf("Error in vertex shader: %s\n", info_log);
			ret_code = -1;
			goto delete_vertex;
		}

		glAttachShader(this->shader_program, vertex_shader);
	} else
		printf("Empty\n");

	if (!this->geometry_file.empty()) {
		geo_b = true;

		src_code = load_shader_program_from_file(this->geometry_file);
		c_string = src_code.c_str();

		/* Compile the vertex shader */
		geometry_shader = glCreateShader(GL_GEOMETRY_SHADER);
		glShaderSource(geometry_shader, 1, &c_string, NULL);
		glCompileShader(geometry_shader);
		glGetShaderiv(geometry_shader, GL_COMPILE_STATUS, &success);

		if (!success) {
			glGetShaderInfoLog(geometry_shader, 512, NULL, info_log);
			fprintf(stderr, "%s\n", info_log);
			ret_code = -1;
			goto delete_geo;
		}

		glAttachShader(this->shader_program, geometry_shader);

	}

	if (!this->fragment_file.empty()) {
		frag_b = true;

		src_code = load_shader_program_from_file(this->fragment_file);
		c_string = src_code.c_str();

		/* Compile the vertex shader */
		fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
		glShaderSource(fragment_shader, 1, &c_string, NULL);
		glCompileShader(fragment_shader);
		glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);

		if (!success) {
			glGetShaderInfoLog(fragment_shader, 512, NULL, info_log);
			fprintf(stderr, "%s\n", info_log);
			ret_code = -1;
			goto delete_frag;
		}

		glAttachShader(this->shader_program, fragment_shader);
	}

	glLinkProgram(this->shader_program);

	glGetShaderiv(this->shader_program, GL_LINK_STATUS, &success);
	if (!success) {
		glGetShaderInfoLog(this->shader_program, 512, NULL, info_log);
		printf("%s\n", info_log);
		ret_code = -2;
	}

	this->compiled = true;

delete_frag:
	if (frag_b)
		glDeleteShader(fragment_shader);
delete_geo:
	if (geo_b)
		glDeleteShader(geometry_shader);
delete_vertex:
	if (vertex_b)
		glDeleteShader(vertex_shader);

	if (ret_code) {
		this->compiled = false;
		glDeleteProgram(this->shader_program);
		this->shader_program = 0;
	}
	return ret_code;
}

void OpenGlShaderProgram::unload()
{
	if (this->compiled) {
		glDeleteProgram(this->shader_program);
	}

	this->compiled = false;
}

int OpenGlShaderProgram::use()
{
	if (!this->compiled)
		return -1;

	glUseProgram(this->shader_program);
	this->setUniformMat4("projection_view_matrix", GlobalCanvasSettings::getPVMatrix());

	return 0;
}

void OpenGlShaderProgram::setUniformFloat(const std::string &uniform, float value)
{
	int loc = this->getUniformLoc(uniform);
	if (loc >= 0) {
		glUniform1f(loc, value);
	}
}

void OpenGlShaderProgram::setUniformInt(const std::string &uniform, int value)
{
	int loc = this->getUniformLoc(uniform);
	if (loc >= 0) {
		glUniform1i(loc, value);
	}
}

void OpenGlShaderProgram::setUniformVec2(const std::string &uniform, float *vector)
{
	int loc = this->getUniformLoc(uniform);
	if (loc >= 0) {
		glUniform2fv(loc, 1, vector);
	}
}

void OpenGlShaderProgram::setUniformMat4(const std::string &uniform, const glm::mat4 &matrix)
{
	int loc = this->getUniformLoc(uniform);
	if (loc >= 0) {
		glUniformMatrix4fv(loc, 1, GL_FALSE, &matrix[0][0]);
	}
}

void OpenGlShaderProgram::setUniformVec4(const std::string &uniform, const glm::vec4 &vector)
{
	int loc = this->getUniformLoc(uniform);
	if (loc >= 0) {
		glUniform4fv(loc, 1, &vector.x);
	}
}

int OpenGlShaderProgram::getUniformLoc(const std::string &uniform)
{
	if (!this->compiled)
		return -1;

	return glGetUniformLocation(this->shader_program, uniform.c_str());
}
