#include "render.h"
#include "state.h"
#include "../platform.h"
#include "../lib/math.h"
#include "../debug/logger.h"
#include "gl_helpers.h"
#include "string.h"
#include "../camera.h" // @Cleanup: remove dependency from camera

r_state r_render_state;


void r_init()
{
	// OpenGL debug messages
	glEnable(GL_DEBUG_OUTPUT);
	glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
	glDebugMessageCallback(glDebugOutput, nullptr);
	glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, nullptr, GL_TRUE);

	// Depth test
	glEnable(GL_DEPTH_TEST);
	// Face culling
	glEnable(GL_CULL_FACE);
	// Texture pixel alignment (default is 4, so rgb textures should have every pixel padded to 4 bytes. With this we can have 3 byte pixels with no padding between eachother)
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

	// Blending
	glEnable(GL_BLEND);
	glBlendEquation(GL_FUNC_ADD);
	glBlendFuncSeparate(GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

	// Cubemap
	glEnable(GL_TEXTURE_CUBE_MAP_SEAMLESS);


	// Load shaders
	if(!r_shader_from_files(&r_render_state.shader_2d, "shaders/2d.vert", "shaders/2d.frag"))
	{
		LOG(LOG_ERROR, "Cannot load 2D shader.");
	}
	if(!r_shader_from_files(&r_render_state.shader_postprocessing, "shaders/postprocessing.vert", "shaders/postprocessing.frag"))
	{
		LOG(LOG_ERROR, "Cannot load postprocessing shader.");
	}
	if(!r_shader_from_files(&r_render_state.shader_pbr, "shaders/pbr.vert", "shaders/pbr.frag"))
	{
		LOG(LOG_ERROR, "Cannot load pbr shader.");
	}
	if(!r_shader_from_files(&r_render_state.shader_shadow_map, "shaders/shadow_map.vert", "shaders/shadow_map.frag"))
	{
		LOG(LOG_ERROR, "Cannot load shadow_map shader.");
	}
	if(!r_shader_from_files(&r_render_state.shader_environment_map, "shaders/environment_map.vert", "shaders/environment_map.frag"))
	{
		LOG(LOG_ERROR, "Cannot load environment_map shader.");
	}

	// Screen size
	u32 width, height;
	p_window_dimensions(&width, &height);
	glViewport(0, 0, width, height);


	/* We have to apply some post-processing effects only to the 3D world
	 * and some effects to both the 3D world and the HUD.
	 *
	 * So we make 2 framebuffers:
	 *     1. target for 3D world
	 *     2. target for HUD
	 * After rendering, we apply the correct post-processing effects and
	 * merge them toghether.
	 */
	// Init framebuffers
	v2s size = {width, height};
	r_render_state.framebuffer_SCREEN = {.gl_id = 0, .size = size, .flags = 0}; // This is a special framebuffer, because it's already there. We don't need to create it
	r_render_state.framebuffer_HUD = r_framebuffer_create(size, 0);
	r_render_state.framebuffer_3D  = r_framebuffer_create(size, R_FRAMEBUFFER_DEPTH);

	// Default framebuffer
	r_framebuffer_select(&r_render_state.framebuffer_SCREEN);

	// Init fullscreen quad
	glGenVertexArrays(1, &r_render_state.gl_screen_quad_VAO);
	glBindVertexArray(r_render_state.gl_screen_quad_VAO);
	glGenBuffers(1, &r_render_state.gl_screen_quad_VBO);
	glBindBuffer(GL_ARRAY_BUFFER, r_render_state.gl_screen_quad_VBO);
	v2 quad_vertices[2*6] =
	{
	//  position       UV coords
		v2{-1,  1},    v2{0, 1},
		v2{-1, -1},    v2{0, 0},
		v2{ 1, -1},    v2{1, 0},
		v2{-1,  1},    v2{0, 1},
		v2{ 1, -1},    v2{1, 0},
		v2{ 1,  1},    v2{1, 1}
	};
	glBufferData(GL_ARRAY_BUFFER, 2*6 * sizeof(v2), quad_vertices, GL_STATIC_DRAW);
	glEnableVertexAttribArray(0);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(v2), (void*)(0));
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2*sizeof(v2), (void*)(sizeof(v2)));

	// Render state
	r_size_update(width, height);
}

void r_deinit()
{
	r_framebuffer_destroy(&r_render_state.framebuffer_HUD);
	r_framebuffer_destroy(&r_render_state.framebuffer_3D);

	glDeleteBuffers(1, &r_render_state.gl_screen_quad_VBO);
	glDeleteVertexArrays(1, &r_render_state.gl_screen_quad_VAO);
}

void r_size_update(u32 width, u32 height)
{
	// Screen size
	r_render_state.width  = width;
	r_render_state.height = height;
	// Update shaders
	glUseProgram(r_render_state.shader_2d.id);
	glUniform1i(r_render_state.shader_2d.width , width);
	glUniform1i(r_render_state.shader_2d.height, height);

	// Update framebuffers size
	v2s size = {width, height};
	r_render_state.framebuffer_SCREEN.size = size;
	r_framebuffer_update_size(&r_render_state.framebuffer_HUD, size);
	r_framebuffer_update_size(&r_render_state.framebuffer_3D , size);
}

void r_time_update(f64 time)
{
	r_render_state.time = time;
}




void r_clear(v4 color)
{
	glClearColor(color.r, color.g, color.b, color.a);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

void r_swap()
{
	p_graphics_swap();
}



r_scene r_scene_create()
{
	r_scene scene;

	scene.view_matrix         = m4_identity;
	scene.view_matrix_inverse = m4_identity;

	scene.projection_matrix         = m4_identity;
	scene.projection_matrix_inverse = m4_identity;

	scene.objects = NULL;
	scene.objects_count = 0;


	// Init lights memory
	memset(&scene.lights, 0, sizeof(r_light_container));
	glGenBuffers(1, &scene.gl_lights);
	glBindBuffer(GL_UNIFORM_BUFFER, scene.gl_lights);
	glBufferData(GL_UNIFORM_BUFFER, sizeof(r_light_container), &scene.lights, GL_STATIC_DRAW);
	glBindBufferBase(GL_UNIFORM_BUFFER, 0, scene.gl_lights);

	return scene;
}

void r_scene_destroy(r_scene *scene)
{
	glDeleteBuffers(1, &scene->gl_lights);
	r_cubemap_destroy(&scene->environment_map);
}

void r_scene_set_view(r_scene *scene, m4 m)
{
	scene->view_matrix         = m;
	scene->view_matrix_inverse = inverse(m);
}

void r_scene_set_projection(r_scene *scene, m4 m)
{
	scene->projection_matrix         = m;
	scene->projection_matrix_inverse = inverse(m);
}

void r_update_lights(r_scene *scene)
{
	glBindBuffer(GL_UNIFORM_BUFFER, scene->gl_lights);
	glBufferData(GL_UNIFORM_BUFFER, sizeof(r_light_container), &scene->lights, GL_STATIC_DRAW);
}



r_object build_basic_cube()
{
	r_object object = r_object_allocate(1);

	object.meshes[0] = (r_mesh*)p_alloc(sizeof(r_mesh));
	*object.meshes[0] = r_mesh_build_cube();

	object.mesh_material[0] = (r_material*)p_alloc(sizeof(r_material));
	memset(object.mesh_material[0], 0, sizeof(r_material));
	object.mesh_material[0]->shader = &r_render_state.shader_pbr;
	object.mesh_material[0]->albedo_factor  = {1,1,1,1};
	object.mesh_material[0]->emissive_factor  = {1,1,1,0};

	return object;
}

void r_render_scene(r_scene *scene)
{
	// Render shadow maps
	scene->shadow_map = r_build_shadow_map_sun(scene, &scene->lights.sun_lights[0]);

	// Render environment map
	glDisable(GL_CULL_FACE);
	glDisable(GL_DEPTH_TEST);
	static r_object cube = build_basic_cube();
	cube.position = extract_column(scene->view_matrix_inverse, 3).xyz;
	glUseProgram(r_render_state.shader_environment_map.id);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_CUBE_MAP, scene->environment_map.gl_id);
	glUniform1i(r_render_state.shader_environment_map.has_environment_map, 1);
	glUniform1i(r_render_state.shader_environment_map.environment_map    , 0);
	r_render_object(scene, &cube, &r_render_state.shader_environment_map);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_CULL_FACE);

	// Render objects
	glUseProgram(r_render_state.shader_pbr.id);

	// Set shadow map
	// @Cleanup: this should be in r_render_objects
	glActiveTexture(GL_TEXTURE5);
	glBindTexture(GL_TEXTURE_2D, scene->shadow_map.gl_depth_texture);
	glUniform1i       (r_render_state.shader_pbr.has_shadow_map, 1);
	glUniform1i       (r_render_state.shader_pbr.shadow_map    , 5);
	glUniformMatrix4fv(r_render_state.shader_pbr.shadow_matrix , 1, GL_TRUE, (const f32 *)(scene->shadow_map.view_matrix.E));

	// Environment map
	glActiveTexture(GL_TEXTURE6);
	glBindTexture(GL_TEXTURE_CUBE_MAP, scene->environment_map.gl_id);
	glUniform1i(r_render_state.shader_pbr.has_environment_map, 1);
	glUniform1i(r_render_state.shader_pbr.environment_map    , 6);

	// Render objects
	for(u32 i = 0; i < scene->objects_count; i++)
	{
		r_render_object(scene, &scene->objects[i]);
	}

	glUniform1i(r_render_state.shader_pbr.has_shadow_map, 0);
	glUniform1i(r_render_state.shader_pbr.has_environment_map, 0);
	r_free_shadow_map(&scene->shadow_map);
}





// Single object rendering functions (batched funcs are below)
void r_render_object(r_scene *scene, r_object *object, r_shader *shader_override)
{
	// @Performance: Minimize draw calls by batching mesh rendering
	for(u32 i = 0; i < object->count; i++)
	{
		r_mesh     *mesh     = object->meshes[i];
		r_material *material = object->mesh_material[i];
		r_shader   *shader   = material->shader;
		if(shader_override)
			shader = shader_override;

		glUseProgram(shader->id);

		glUniform1f(shader->time  , (f32)r_render_state.time  );
		glUniform1f(shader->width , (f32)r_render_state.width );
		glUniform1f(shader->height, (f32)r_render_state.height);

		// Transform matrices
		m4 projview_matrix = scene->projection_matrix * scene->view_matrix;
		m4 projview_matrix_inverse = inverse(projview_matrix);
		glUniformMatrix4fv(shader->view_matrix,         1, GL_TRUE, (const f32 *)(&projview_matrix));
		glUniformMatrix4fv(shader->view_matrix_inverse, 1, GL_TRUE, (const f32 *)(&projview_matrix_inverse));

		m4 model_transform = r_object_mesh_transform_matrix(object, i);
		glUniformMatrix4fv(shader->model_matrix, 1, GL_TRUE, (const f32 *)(&model_transform));

		// Textures
		// Albedo
		glUniform1i(shader->has_albedo_texture, material->albedo_texture != NULL);
		if(material->albedo_texture)
		{
			glActiveTexture(GL_TEXTURE0);
			glBindTexture(GL_TEXTURE_2D, material->albedo_texture->gl_id);
			glUniform1i(shader->albedo_texture, 0);
		}
		glUniform4f(shader->albedo_factor, material->albedo_factor.r, material->albedo_factor.g, material->albedo_factor.b, material->albedo_factor.a);

		// Metallic
		glUniform1i(shader->has_metallic_texture, material->metallic_texture != NULL);
		if(material->metallic_texture)
		{
			glActiveTexture(GL_TEXTURE1);
			glBindTexture(GL_TEXTURE_2D, material->metallic_texture->gl_id);
			glUniform1i(shader->metallic_texture, 1);
		}
		glUniform1f(shader->metallic_factor, material->metallic_factor);

		// Roughness
		glUniform1i(shader->has_roughness_texture, material->roughness_texture != NULL);
		if(material->roughness_texture)
		{
			glActiveTexture(GL_TEXTURE2);
			glBindTexture(GL_TEXTURE_2D, material->roughness_texture->gl_id);
			glUniform1i(shader->roughness_texture, 2);
		}
		glUniform1f(shader->roughness_factor, material->roughness_factor);

		// Normal
		glUniform1i(shader->has_normal_texture, material->normal_texture != NULL);
		if(material->normal_texture)
		{
			glActiveTexture(GL_TEXTURE3);
			glBindTexture(GL_TEXTURE_2D, material->normal_texture->gl_id);
			glUniform1i(shader->normal_texture, 3);
		}

		// Emissive
		glUniform1i(shader->has_emissive_texture, material->emissive_texture != NULL);
		if(material->emissive_texture)
		{
			glActiveTexture(GL_TEXTURE4);
			glBindTexture(GL_TEXTURE_2D, material->emissive_texture->gl_id);
			glUniform1i(shader->emissive_texture, 4);
		}
		glUniform4f(shader->emissive_factor, material->emissive_factor.r, material->emissive_factor.g, material->emissive_factor.b, material->emissive_factor.a);



		// Draw call
		glBindVertexArray(mesh->gl_VAO);
		glDrawElements(GL_TRIANGLES, mesh->indices_count, GL_UNSIGNED_INT, (void*)(0));
	}
}

void r_render_object_wireframe(r_scene *scene, r_object *object)
{
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	r_render_object(scene, object);
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

void r_render_line(r_scene *scene, v3 a, v3 b, v4 a_color, v4 b_color, f32 thickness)
{
	m4 projview_matrix = scene->projection_matrix * scene->view_matrix;
	v4 a_trans = projview_matrix * V4(a, 1);
	v4 b_trans = projview_matrix * V4(b, 1);
	a_trans /= a_trans.w;
	b_trans /= b_trans.w;
	if(a_trans.z > 1 || b_trans.z > 1)
		return;
	v2 a_2d = a_trans.xy;
	v2 b_2d = b_trans.xy;
	a_2d = (v2{.5,.5} + v2{.5,-0.5} * a_2d) * v2{(f32)r_render_state.width, (f32)r_render_state.height};
	b_2d = (v2{.5,.5} + v2{.5,-0.5} * b_2d) * v2{(f32)r_render_state.width, (f32)r_render_state.height};

	r_2d_immediate_segment(a_2d, b_2d, a_color, b_color, thickness); // @Cleanup: This should be independent from the 2D drawing functions, probably.
}




// Batch render functions
void r_render_objects(r_scene *scene, u64 count, r_object **objects)
{
	// @Feature: implement
}




r_shadow_map r_build_shadow_map_sun(r_scene *scene, r_sun_light *sun)
{
	glCullFace(GL_FRONT);

	r_shadow_map sm;
	u32 width = 2*1024;
	u32 height = 2*1024;

	// Init depth texture/framebuffer
	glGenTextures(1, &sm.gl_depth_texture);
	glBindTexture(GL_TEXTURE_2D, sm.gl_depth_texture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glBindTexture(GL_TEXTURE_2D, 0);

	glGenFramebuffers(1, &sm.gl_FBO);
	glBindFramebuffer(GL_FRAMEBUFFER, sm.gl_FBO);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, sm.gl_depth_texture, 0);
	glDrawBuffer(GL_NONE);

	glBindTexture(GL_TEXTURE_2D, 0);

	// Compute light frustum
	m4 projview_matrix = scene->projection_matrix * scene->view_matrix;
	m4 projview_matrix_inverse = inverse(projview_matrix);
	v4 center_point_4 = projview_matrix_inverse * v4{0, 0, 0, 1};
	v3 center_point = center_point_4.xyz / center_point_4.w;
	f32 size = 1;
	v4 bottom = projview_matrix_inverse * v4{ 0,-1, 0, 1};
	v4 top    = projview_matrix_inverse * v4{ 0, 1, 0, 1};
	v4 left   = projview_matrix_inverse * v4{-1, 0, 0, 1};
	v4 right  = projview_matrix_inverse * v4{ 1, 0, 0, 1};
	v4 front  = projview_matrix_inverse * v4{ 0, 0,-1, 1};
	v4 back   = projview_matrix_inverse * v4{ 0, 0, 1, 1};
	size = maximum(size, length(top.xyz/top.w - bottom.xyz/bottom.w));
	size = maximum(size, length(right.xyz/right.w - left.xyz/left.w));
	size = maximum(size, length(back.xyz/back.w - front.xyz/front.w));
	size /= 2; // @Cleanup: hack to have better precision. There are multiple ways to do it right: smallest box around all objects, cascaded shadow maps


	v3 light_position = center_point - size * sun->direction;
	m4 light_camera_matrix = r_view_matrix(light_position, sun->direction, v3{0, 0, 1});
	m4 projection_matrix = r_orthographic_matrix(-size/2, size/2, -size/2, size/2, 0.1, 100.0);

	sm.view_matrix = projection_matrix * light_camera_matrix;

	// Render shadow map
	glViewport(0, 0, width, height);
	glClear(GL_DEPTH_BUFFER_BIT);

	// @Feature @Cleanup: Some objects might not need to be drawn. Add a "hidden" flag. Maybe take the list of objects to render as an argument
	m4 old_view = scene->view_matrix;
	m4 old_proj = scene->projection_matrix;
	r_scene_set_view      (scene, sm.view_matrix);
	r_scene_set_projection(scene, m4_identity);
	for(u32 i = 0; i < scene->objects_count; i++)
	{
		if(scene->objects[i].has_shadow)
		{
			r_render_object(scene, &scene->objects[i], &r_render_state.shader_shadow_map);
		}
	}
	r_scene_set_view      (scene, old_view);
	r_scene_set_projection(scene, old_proj);

	// Restore framebuffer
	r_framebuffer_select(r_render_state.current_framebuffer);

	glCullFace(GL_BACK);
	return sm;
}

void r_free_shadow_map(r_shadow_map *sm)
{
	glDeleteTextures(1, &sm->gl_depth_texture);
	glDeleteFramebuffers(1, &sm->gl_FBO);
}




void r_merge_and_postprocess()
{
	glDisable(GL_DEPTH_TEST);
	glEnable(GL_FRAMEBUFFER_SRGB);

	glUseProgram(r_render_state.shader_postprocessing.id);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, r_render_state.framebuffer_3D.color_texture.gl_id);
	glUniform1i(r_render_state.shader_postprocessing.texture[0], 0);

	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, r_render_state.framebuffer_HUD.color_texture.gl_id);
	glUniform1i(r_render_state.shader_postprocessing.texture[1], 1);

	glBindVertexArray(r_render_state.gl_screen_quad_VAO);
	glDrawArrays(GL_TRIANGLES, 0, 6);

	glEnable(GL_DEPTH_TEST);
	glDisable(GL_FRAMEBUFFER_SRGB);
}



void r_framebuffer_select(r_framebuffer *fb)
{

	glBindFramebuffer(GL_FRAMEBUFFER, fb->gl_id);
	glViewport(0, 0, fb->size.x, fb->size.y);

	glUseProgram(r_render_state.shader_2d.id);
	glUniform1i(r_render_state.shader_2d.width , fb->size.x);
	glUniform1i(r_render_state.shader_2d.height, fb->size.y);

	r_render_state.current_framebuffer = fb;
}
