#include "OpenGLWrapper.h"
#include "ShaderType.h"
#include "ResourceLoader.h"

// settings
const unsigned int _SCR_WIDTH = 800;
const unsigned int _SCR_HEIGHT = 600;

// lighting
glm::vec3 _lightPos(1.2f, 20.0f, 2.0f);

Mesh::Mesh(GLuint _nVertex, GLuint _VAO, GLuint *_VBOs)
{
	nVertex = _nVertex;
	VAO = _VAO;
	VBOs = _VBOs;
}

GLuint Mesh::get_vertex_count()
{
	return nVertex;
}
GLuint Mesh::get_VAO()
{
	return VAO;
}
GLuint *Mesh::get_VBOs()
{
	return VBOs;
}

void draw_mesh(Mesh &mesh)
{
	glBindVertexArray(mesh.get_VAO());

	glDrawArrays(GL_TRIANGLES, 0, mesh.get_vertex_count());

	glBindVertexArray(0);
}

Material::Material(GLuint _prog, GLuint _diffuseMap, GLuint _specularMap)
{
	prog = _prog;

	diffuseMap = _diffuseMap;
	specularMap = _specularMap;
}

GLuint Material::get_program()
{
	return prog;
}
GLuint Material::get_diffuseMap()
{
	return diffuseMap;
}
GLuint Material::get_specularMap()
{
	return specularMap;
}

RenderObject::RenderObject(Mesh * _mesh)
{
	translate = glm::vec3(0.0f, 0.0f, 0.0f);
	rotate = glm::vec3(0.0f, 0.0f, 0.0f);
	scale = glm::vec3(1.0f, 1.0f, 1.0f);

	mesh = _mesh;

	update_model_matrix();
}

Material *RenderObject::get_material()
{
	return material;
}

GLuint RenderObject::get_vertex_count()
{
	return mesh->get_vertex_count();
}

glm::mat4 RenderObject::get_model_matrix()
{
	return model;
}

void RenderObject::update_model_matrix()
{
	model = glm::mat4(1.0f);
	model = glm::translate(model, translate);

	model = glm::rotate(model, glm::radians(rotate.x), glm::vec3(1.0f, 0.0f, 0.0f));
	model = glm::rotate(model, glm::radians(rotate.y), glm::vec3(0.0f, 1.0f, 0.0f));
	model = glm::rotate(model, glm::radians(rotate.z), glm::vec3(0.0f, 0.0f, 1.0f));

	model = glm::scale(model, scale);
}

void RenderObject::set_translate(glm::vec3 _translate)
{
	translate = _translate;

	update_model_matrix();
}

void RenderObject::set_rotate(glm::vec3 _rotate)
{
	rotate = _rotate;

	update_model_matrix();
}

void RenderObject::set_scale(glm::vec3 _scale)
{
	scale = _scale;

	update_model_matrix();
}

void RenderObject::move(glm::vec3 _delta)
{
	translate += _delta;

	update_model_matrix();
}

void RenderObject::move(glm::vec3 _direction, glm::vec1 _velocity)
{
	translate += glm::vec3(_velocity *_direction.x, _velocity *_direction.y, _velocity *_direction.z);

	update_model_matrix();
}

void RenderObject::set_material(Material *_material)
{
	material = _material;
}

void RenderObject::render(Camera &camera)
{
	auto prog = material->get_program();
	glUseProgram(prog);

	set_uniform_value(prog, "light.position", _lightPos);
	set_uniform_value(prog, "viewPos", camera.Position);

	glm::vec3 a = { 0.2f, 0.2f, 0.2f };
	glm::vec3 d = { 0.5f, 0.5f, 0.5f };
	glm::vec3 f = { 1.0f, 1.0f, 1.0f };
	set_uniform_value(prog, "light.ambient", a);
	set_uniform_value(prog, "light.diffuse", d);
	set_uniform_value(prog, "light.specular", f);

	set_uniform_value(prog, "material.shininess", glm::fvec1{ 64.0f });

	glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)_SCR_WIDTH / (float)_SCR_HEIGHT, 0.1f, 500.0f);
	glm::mat4 view = camera.GetViewMatrix();
	set_uniform_value(prog, "projection", projection);
	set_uniform_value(prog, "view", view);
	set_uniform_value(prog, "model", model);

	{
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, material->get_diffuseMap());
	}

	{
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, material->get_specularMap());
	}

	draw_mesh(*mesh);

	glUseProgram(0);
}

RenderObject *make_render_object(Mesh *mesh)
{
	RenderObject *ro = new RenderObject(mesh);

	return ro;
};

GLuint build_program(const std::string name)
{
	std::string shaderSources[5];
	shaderSources[0] = FileSystem::read_file("../Shaders/" + name + "/" + name + ".vert");
	shaderSources[1] = FileSystem::read_file("../Shaders/" + name + "/" + name + ".frag");

	std::vector<GLint> shaderIDs;
	compile_shaders(&shaderIDs, shaderSources);

	auto programID = glCreateProgram();
	for (auto i : shaderIDs)
	{
		glAttachShader(programID, i);
	}
	glLinkProgram(programID);
	for (auto i : shaderIDs)
	{
		glDeleteShader(i);
	}

	return programID;
}

void compile_shaders(std::vector<GLint> *shaderIDs, const std::string *shaderSources)
{
	shaderIDs->push_back(compile_shader(GL_VERTEX_SHADER, &shaderSources[0]));
	shaderIDs->push_back(compile_shader(GL_FRAGMENT_SHADER, &shaderSources[1]));
}

GLint compile_shader(const GLint shaderType, const std::string *shaderSource)
{
	GLuint shaderID;
	switch (shaderType)
	{
	case (GL_VERTEX_SHADER): shaderID = glCreateShader(GL_VERTEX_SHADER); break;
	case (GL_FRAGMENT_SHADER): shaderID = glCreateShader(GL_FRAGMENT_SHADER); break;
	default:
		GLchar infoLog[512];
		log_error(infoLog, "SHADER::CAN'T_FIND_SHADER_TYPE");

		break;
	}

	const char *c_str = (*shaderSource).c_str();
	glShaderSource(shaderID, 1, &c_str, NULL);
	glCompileShader(shaderID);

	GLint success;
	glGetShaderiv(shaderID, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		GLchar infoLog[512];
		glGetShaderInfoLog(shaderID, 512, NULL, infoLog);

		log_error(infoLog, "SHADER::" + parse_shaderType(shaderType) + "::COMPILATION_ERROR");
	}

	return shaderID;
}

GLint link_program(const GLint *shaderIDs)
{
	auto programID = glCreateProgram();
	glAttachShader(programID, shaderIDs[0]);
	glAttachShader(programID, shaderIDs[1]);
	glLinkProgram(programID);

	GLint success;
	glGetProgramiv(programID, GL_LINK_STATUS, &success);
	if (!success)
	{
		GLchar infoLog[512];
		log_error(infoLog, "PROGRAM::LINK_FAILED");
	}

	glDeleteShader(shaderIDs[0]);
	glDeleteShader(shaderIDs[1]);

	return programID;
}

void set_uniform_value(GLuint &prog, const char *name, glm::vec1 value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniform1fv(uniform, 1, &value.x);
}
void set_uniform_value(GLuint &prog, const char *name, glm::ivec1 value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniform1iv(uniform, 1, &value.x);
}
void set_uniform_value(GLuint &prog, const char *name, glm::vec2 &value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniform2fv(uniform, 1, &value.x);
}
void set_uniform_value(GLuint &prog, const char *name, glm::ivec2 &value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniform2iv(uniform, 1, &value.x);
}
void set_uniform_value(GLuint &prog, const char *name, glm::vec3 &value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniform3fv(uniform, 1, &value.x);
}
void set_uniform_value(GLuint &prog, const char *name, glm::ivec3 &value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniform3iv(uniform, 1, &value.x);
}
void set_uniform_value(GLuint &prog, const char *name, glm::vec4 &value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniform4fv(uniform, 1, &value.x);
}
void set_uniform_value(GLuint &prog, const char *name, glm::ivec4 &value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniform4iv(uniform, 1, &value.x);
}

void set_uniform_value(GLuint &prog, const char *name, glm::mat4 &value)
{
	auto uniform = glGetUniformLocation(prog, name);
	glUniformMatrix4fv(uniform, 1, GL_FALSE, &value[0][0]);
}

GLuint allocate_VBO(const GLuint attribIndex, std::vector<glm::vec3> *VBO)
{
	GLuint VBOIndex = 0;

	glGenBuffers(1, &VBOIndex);
	glBindBuffer(GL_ARRAY_BUFFER, VBOIndex);
	glBufferData(GL_ARRAY_BUFFER, VBO->size() * sizeof(glm::vec3), &(VBO->front()), GL_STATIC_DRAW);

	glEnableVertexAttribArray(attribIndex);
	glVertexAttribPointer(attribIndex, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);

	glBindBuffer(GL_ARRAY_BUFFER, 0);

	return VBOIndex;
}

GLuint allocate_VBO(const GLuint attribIndex, std::vector<glm::vec2> *VBO)
{
	GLuint VBOIndex = 0;

	glGenBuffers(1, &VBOIndex);
	glBindBuffer(GL_ARRAY_BUFFER, VBOIndex);
	glBufferData(GL_ARRAY_BUFFER, VBO->size() * sizeof(glm::vec2), &(VBO->front()), GL_STATIC_DRAW);

	glEnableVertexAttribArray(attribIndex);
	glVertexAttribPointer(attribIndex, 2, GL_FLOAT, GL_FALSE, 0, (void*)0);

	glBindBuffer(GL_ARRAY_BUFFER, 0);

	return VBOIndex;
}

GLuint *allocate_VBOs(GLuint VAO, std::vector<std::vector<glm::vec3> *> &vertexInfoVec3, std::vector<std::vector<glm::vec2> *> &vertexInfoVec2)
{
	auto size = vertexInfoVec3.size() + vertexInfoVec2.size();
	GLuint *VBOindicies = new GLuint[size];

	glBindVertexArray(VAO);

	GLuint i = 0;
	for (i = 0; i < vertexInfoVec3.size(); i++)
	{
		VBOindicies[i] = allocate_VBO(i, vertexInfoVec3.at(i));
	}
	for (i = vertexInfoVec3.size(); i < size; i++)
	{
		VBOindicies[i] = allocate_VBO(i, vertexInfoVec2.at(i - vertexInfoVec3.size()));
	}

	glBindVertexArray(0);

	return VBOindicies;
}

GLuint allocate_VAO()
{
	GLuint VAO = 0;

	glGenVertexArrays(1, &VAO);

	return VAO;
}

Mesh *make_mesh(const std::string fileName)
{
	std::vector<glm::vec3> vertices;
	std::vector<glm::vec2> vertexTexCoord;
	std::vector<glm::vec3> vertexNormals;

	const std::string ext = get_extension(fileName);
	if (ext.compare("obj") == 0)
	{
		openObj(fileName, vertices, vertexTexCoord, vertexNormals);
	}
	else
	{
		std::cout << "Can't Open " + fileName + " Extension." << std::endl;
	}

	std::vector<std::vector<glm::vec3> *> vertexInfoVec3;
	std::vector<std::vector<glm::vec2> *> vertexInfoVec2;

	vertexInfoVec3.push_back(&vertices);
	if (vertexTexCoord.size() > 0)
	{
		vertexInfoVec2.push_back(&vertexTexCoord);
	}
	if (vertexNormals.size() > 0)
	{
		vertexInfoVec3.push_back(&vertexNormals);
	}

	auto VAO = allocate_VAO();
	auto VBOs = allocate_VBOs(VAO, vertexInfoVec3, vertexInfoVec2);

	Mesh *m = new Mesh(vertices.size(), VAO, VBOs);

	return m;
}

GLuint load_image(const std::string fileName)
{
	int width, height, nrChannels;
	GLuint textureID;

	Image *tex = load_Image(fileName, &width, &height, &nrChannels);
	if (tex != NULL && tex->getData() != NULL)
	{
		GLenum format;
		if (nrChannels == 1)
			format = GL_RED;
		else if (nrChannels == 3)
			format = GL_RGB;
		else if (nrChannels == 4)
			format = GL_RGBA;

		glGenTextures(1, &textureID);
		glBindTexture(GL_TEXTURE_2D, textureID);

		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_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

		glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, tex->getData());
		glGenerateMipmap(GL_TEXTURE_2D);
	}

	free_image(tex);

	return textureID;
}