#include "shader.h"
#include "../debug/logger.h"

#define STB_INCLUDE_IMPLEMENTATION
#define STB_INCLUDE_LINE_GLSL
#include "stb_include.h"

// Internal functions
void r_shader_set_uniform_locations(r_shader *shader);





bool r_shader_from_files(r_shader *shader, const char *vertex, const char *fragment, const char *geometry, const char *tesseletion_control, const char *tesseletion_evaluation, const char *compute)
{
	const int count = 6;
	char *source[count] = {NULL, NULL, NULL, NULL, NULL, NULL};
	const char *filename[count] = {vertex, fragment, geometry, tesseletion_control, tesseletion_evaluation, compute};
	const char *type[count] = {"vertex", "fragment", "geometry", "tesseletion_control", "tesseletion_evaluation", "compute"};

	// Load sources
	bool has_error = false;
	char error[256];
	for(int i = 0; i < count; i++)
	{
		if(filename[i])
		{
			char path[4096];
			u32 len = strlen(filename[i]);
			memcpy(path, filename[i], len+1);
			int last_slash = len;
			while(last_slash > 0)
			{
				if(filename[i][last_slash] == '/')
					break;
				last_slash--;
			}
			path[last_slash] = '\0';

			source[i] = stb_include_file((char*)filename[i], NULL, path, error);
			if(!source[i])
			{
				has_error = true;
				LOG(LOG_ERROR, "Error loading %s shader: %s", type[i], error);
				break;
			}
		}
	}

	// Compile shader
	if(!has_error)
	{
		has_error = !r_shader_from_text(shader, source[0], source[1], source[2], source[3], source[5], source[5]);
	}

	// Cleanup
	for(int i = 0; i < count; i++)
	{
		if(source[i])
			free(source[i]);
	}

	return !has_error;
}


bool r_shader_from_text(r_shader *shader, const char *vertex, const char *fragment, const char *geometry, const char *tesseletion_control, const char *tesseletion_evaluation, const char *compute)
{
	int count = 6;
	GLuint type[count] = {GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER, GL_TESS_CONTROL_SHADER, GL_TESS_EVALUATION_SHADER, GL_COMPUTE_SHADER};
	const char *source[count] = {vertex, fragment, geometry, tesseletion_control, tesseletion_evaluation, compute};

	// Create shader program
	shader->id = glCreateProgram();

	bool has_error = false;
	for(int i = 0; i < count; i++)
	{
		if(!source[i])
			continue;

		// Compile current shader source
		GLuint source_id = glCreateShader(type[i]);
		GLint size = strlen(source[i]);
		glShaderSource(source_id, 1, &source[i], &size);
		glCompileShader(source_id);

		// Check for success/errors
		char build_info[2048];
		GLint build_success;
		glGetShaderiv(source_id, GL_COMPILE_STATUS, &build_success);
		if (!build_success)
		{
			glGetShaderInfoLog(source_id, 2048, NULL, build_info);
			LOG(LOG_ERROR, "Cannot compile shader %.*s: %s", (int)size, source, build_info);
			glDeleteShader(source_id);
			has_error = true;
			break;
		}

		// Attach compiler shader to program
		glAttachShader(shader->id, source_id);
	}

	// @Correctness: Clean up shader sources after fail
	if(has_error)
		return false;

	// Link program
	glLinkProgram(shader->id);

	// Check for success/error
	GLint build_success;
	char build_info[512];
	glGetProgramiv(shader->id, GL_LINK_STATUS, &build_success);
	if(!build_success) {
		glGetProgramInfoLog(shader->id, 512, NULL, build_info);
		LOG(LOG_ERROR, "Cannot link shader program: %s", build_info);
		glDeleteShader(shader->id);
		has_error = true;
	}

	// Load uniform locations
	if(!has_error)
		r_shader_set_uniform_locations(shader);

	return !has_error;
}


void r_shader_delete(r_shader *shader)
{
	glDeleteProgram(shader->id);
}





// Internal functions
void r_shader_set_uniform_locations(r_shader *shader)
{
	shader->time   = glGetUniformLocation(shader->id, "time");
	shader->width  = glGetUniformLocation(shader->id, "width");
	shader->height = glGetUniformLocation(shader->id, "height");

	shader->view_matrix          = glGetUniformLocation(shader->id, "view_matrix");
	shader->view_matrix_inverse  = glGetUniformLocation(shader->id, "view_matrix_inverse");
	shader->model_matrix         = glGetUniformLocation(shader->id, "model_matrix");
	shader->camera_position      = glGetUniformLocation(shader->id, "camera_position");

	for(int i = 0; i < R_SHADER_COLOR_MAX; i++)
	{
		char uniform_name[32]; sprintf(uniform_name, "color%d", i);
		shader->color[i] = glGetUniformLocation(shader->id, uniform_name);
	}
	for(int i = 0; i < R_SHADER_TEXTURES_MAX; i++)
	{
		char uniform_name[32];
		sprintf(uniform_name, "has_texture%d", i);
		shader->has_texture     [i] = glGetUniformLocation(shader->id, uniform_name);
		sprintf(uniform_name, "texture%d", i);
		shader->texture         [i] = glGetUniformLocation(shader->id, uniform_name);
		sprintf(uniform_name, "texture_channels%d", i);
		shader->texture_channels[i] = glGetUniformLocation(shader->id, uniform_name);
	}

	shader->lights = glGetUniformBlockIndex(shader->id, "lights");
	if(shader->lights != GL_INVALID_INDEX)
		glUniformBlockBinding(shader->id, shader->lights, 0);
	shader->has_shadow_map = glGetUniformLocation(shader->id, "has_shadow_map");
	shader->shadow_map     = glGetUniformLocation(shader->id, "shadow_map");
	shader->shadow_matrix  = glGetUniformLocation(shader->id, "shadow_matrix");

	shader->has_environment_map = glGetUniformLocation(shader->id, "has_environment_map");
	shader->environment_map     = glGetUniformLocation(shader->id, "environment_map");

	shader->has_albedo_texture = glGetUniformLocation(shader->id, "has_albedo_texture");
	shader->albedo_texture     = glGetUniformLocation(shader->id, "albedo_texture");
	shader->albedo_factor      = glGetUniformLocation(shader->id, "albedo_factor");
	shader->has_metallic_texture = glGetUniformLocation(shader->id, "has_metallic_texture");
	shader->metallic_texture     = glGetUniformLocation(shader->id, "metallic_texture");
	shader->metallic_factor      = glGetUniformLocation(shader->id, "metallic_factor");
	shader->has_roughness_texture = glGetUniformLocation(shader->id, "has_roughness_texture");
	shader->roughness_texture     = glGetUniformLocation(shader->id, "roughness_texture");
	shader->roughness_factor      = glGetUniformLocation(shader->id, "roughness_factor");
	shader->has_normal_texture = glGetUniformLocation(shader->id, "has_normal_texture");
	shader->normal_texture     = glGetUniformLocation(shader->id, "normal_texture");
	shader->has_emissive_texture = glGetUniformLocation(shader->id, "has_emissive_texture");
	shader->emissive_texture     = glGetUniformLocation(shader->id, "emissive_texture");
	shader->emissive_factor      = glGetUniformLocation(shader->id, "emissive_factor");
}
