#include "GL/glew.h"
#include "GLFW/glfw3.h"

#include "pwyazh.h"
#include "pwyazh_GL.h"

#include "common.h"

void render_scene(U32 shader, Mesh *cube, Mesh *plane)
{
	MAT4 model;

	model = mat4_identity();
	shader_set_mat4fv(shader, "model", model); 
	mesh_draw(plane);

	/*
	   model = mat4_identity();
	   model = mat4_make_scale(v3f(10.0f, 0.1f, 10.0f));
	   model = mat4_translate(model, v3f(0.0f, -0.6f, 0.0f));
	   shader_set_mat4fv(shader, "model", model);
	   mesh_draw(cube);
	   */

	model = mat4_identity();
	model = mat4_make_scale(v3f(0.5f, 0.5f, 0.5f));
	model = mat4_translate(model, v3f(0.0f, 1.5f, 0.0f));
	shader_set_mat4fv(shader, "model", model);
	mesh_draw(cube);

	model = mat4_identity();
	model = mat4_make_scale(v3f(0.5f, 0.5f, 0.5f));
	model = mat4_translate(model, v3f(2.0f, 0.0f, 1.0f));
	shader_set_mat4fv(shader, "model", model);
	mesh_draw(cube);

	model = mat4_identity();
	model = mat4_make_scale(v3f(0.25f, 0.25f, 0.25f));
	model = mat4_rotate_angles(model, v3f(60.0f, 0.0f, 0.0f));
	model = mat4_translate(model, v3f(-1.0f, 0.0f, 2.0f));
	shader_set_mat4fv(shader, "model", model);
	mesh_draw(cube);
}

int main(void)
{
	GLFWwindow *window;
	Arena *arena;
	State state;
	F64 time, last_time;
	S32 screen_width = 800, screen_height = 600;
	U32 shadow_width = 1024, shadow_height = 1024;
	Input input;
	U32 shadows_ortho = 1;
	V3F light_pos = v3f(-2.0f, 4.0f, -2.0f);

	glfwSetErrorCallback(error_callback);

	if (glfwInit() == GLFW_FALSE)
		return(1);

	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_SAMPLES, 4);
	window = glfwCreateWindow(screen_width, screen_height, "Shadow mapping", 0, 0);
	if (!window)
		goto error;

	glfwMakeContextCurrent(window);

	if (glewInit() != GLEW_OK)
		goto error;

	glEnable(GL_DEPTH_TEST);
	// glEnable(GL_CULL_FACE);

	/* NOTE(pryazha): Init */
	arena = arena_alloc(Megabytes(64));

	state.camera = (Camera){ v3f(0.0f, 1.0f, 3.0f), 90.0f, 0.1f, 100.0f, 0.0f, 0.0f };

	/* NOTE(pryazha): Meshes */
	Vertex plane_vertices[] = {
		// positions            // normals         // texcoords
		vertex(v3f(-25.0f, -0.5f,  25.0f),  v3f(0.0f, 1.0f, 0.0f),  v2f( 0.0f,  0.0f)),
		vertex(v3f( 25.0f, -0.5f,  25.0f),  v3f(0.0f, 1.0f, 0.0f),  v2f(25.0f,  0.0f)),
		vertex(v3f(-25.0f, -0.5f, -25.0f),  v3f(0.0f, 1.0f, 0.0f),  v2f( 0.0f, 25.0f)),

		vertex(v3f( 25.0f, -0.5f,  25.0f),  v3f(0.0f, 1.0f, 0.0f),  v2f(25.0f,  0.0f)),
		vertex(v3f( 25.0f, -0.5f, -25.0f),  v3f(0.0f, 1.0f, 0.0f),  v2f(25.0f, 25.0f)),
		vertex(v3f(-25.0f, -0.5f, -25.0f),  v3f(0.0f, 1.0f, 0.0f),  v2f( 0.0f, 25.0f)),
	};
	U32 indices[] = { 0, 1, 2, 3, 4, 5 };
	Mesh *plane = mesh_init(arena, plane_vertices, ArrayCount(plane_vertices),
				indices, ArrayCount(indices));
	Mesh *cube = mesh_load_obj(arena, "../../data/models/cube.obj");

	F32 quad_vertices[] = {
		-1.0f, -1.0f,  0.0f,   0.0f, 0.0f,
		1.0f, -1.0f,  0.0f,   1.0f, 0.0f,
		1.0f,  1.0f,  0.0f,   1.0f, 1.0f,
		-1.0f, -1.0f,  0.0f,   0.0f, 0.0f,
		1.0f,  1.0f,  0.0f,   1.0f, 1.0f,
		-1.0f,  1.0f,  0.0f,   0.0f, 1.0f
	};

	U32 quad_vao, vbo;    
	glGenVertexArrays(1, &quad_vao);
	glBindVertexArray(quad_vao);
	glGenBuffers(1, &vbo);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);
	glBufferData(GL_ARRAY_BUFFER, sizeof(quad_vertices), quad_vertices, GL_STATIC_DRAW);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5*sizeof(F32), (void *)0);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5*sizeof(F32), (void *)(3*sizeof(F32)));
	glBindVertexArray(0);

	/* NOTE(pryazha): Shaders */
	U32 debug_quad_shader = create_shader_program("shaders/debug_quad.vert",
						      "shaders/debug_quad.frag");
	U32 simple_depth_shader = create_shader_program("shaders/simple_depth.vert",
							"shaders/simple_depth.frag");
	U32 shadow_shader = create_shader_program("shaders/shadow.vert",
						  "shaders/shadow.frag");
	U32 color_shader = create_shader_program("shaders/color.vert",
						 "shaders/color.frag");

	glUseProgram(shadow_shader);
	shader_set_1i(shadow_shader, "diffuse_texture", 0);
	shader_set_1i(shadow_shader, "shadow_map", 1);
	glUseProgram(0);

	U32 wood_texture = load_texture_gamma("../../data/textures/wood.png", 1);

	U32 depth_map_fbo, depth_map;
	glGenFramebuffers(1, &depth_map_fbo);
	glGenTextures(1, &depth_map);
	glBindTexture(GL_TEXTURE_2D, depth_map);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadow_width,
		     shadow_height, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
	glBindFramebuffer(GL_FRAMEBUFFER, depth_map_fbo);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D,
			       depth_map, 0);
	V4F border_color = v4f(1.0f, 1.0f, 1.0f, 1.0f);
	glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, (const F32 *)&border_color);
	glDrawBuffer(GL_NONE);
	glReadBuffer(GL_NONE);
	if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
		fprintf(stderr, "[ERROR]: Failed to complete depth map framebuffer.\n");
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	last_time = glfwGetTime();

	while (!glfwWindowShouldClose(window)) {
		glfwPollEvents();

		/* NOTE(pryazha): Update */
		process_glfw_keyboard(window, &input);
		glfwGetFramebufferSize(window, &screen_width, &screen_height);

		V3F target = v3f_zero();

		if (key_first_press(input.exit))
			glfwSetWindowShouldClose(window, GLFW_TRUE);

		V3F dv = get_dv_camera_orbital(&input, state.camera.pos,
					       target, state.dt, 3.0f);
		state.camera.pos = v3f_add(state.camera.pos, dv);

		if (key_first_press(input.action_down))
			shadows_ortho = !shadows_ortho;

		input_update_last_state(&input);

		F32 radius, angular_speed, x, z;

		angular_speed = 1.0f;
		radius = 4.0f;
		x = f32_sin(time*angular_speed)*radius;
		z = f32_cos(time*angular_speed)*radius;

		light_pos = v3f(x, 4.0f, z);

		/* NOTE(pryazha): Render */
		MAT4 proj, view, model, light_proj;
		F32 fovx, near, far;

		if (shadows_ortho) {
			near = 1.0f;
			far = 10.0f;
			F32 half_side = 10.0f;
			light_proj = ortho(-half_side, half_side, -half_side, half_side, near, far);
		} else {
			fovx = 90.0f;
			near = 1.0f;
			far = 50.0f;
			light_proj = perspective(fovx, (F32)shadow_width/(F32)shadow_height,
						 near, far);
		}

		V3F up = v3f(0.0f, 1.0f, 0.0f);
		MAT4 light_view = look_at(light_pos, target, up);
		MAT4 light_space_matrix = mat4_mul(light_proj, light_view);

		glUseProgram(simple_depth_shader);
		shader_set_mat4fv(simple_depth_shader, "light_space_matrix", light_space_matrix); 
		glViewport(0, 0, shadow_width, shadow_height);
		glBindFramebuffer(GL_FRAMEBUFFER, depth_map_fbo);
		glClear(GL_DEPTH_BUFFER_BIT);
		// glCullFace(GL_FRONT);
		render_scene(simple_depth_shader, cube, plane);
		// glCullFace(GL_BACK);
		glBindFramebuffer(GL_FRAMEBUFFER, 0);

		glViewport(0, 0, screen_width, screen_height);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

		proj = perspective(state.camera.fovx, (F32)screen_width/(F32)screen_height, 
				   state.camera.near, state.camera.far);
		view = look_at(state.camera.pos, v3f_zero(), v3f(0.0f, 1.0f, 0.0f));

		glUseProgram(shadow_shader);
		shader_set_mat4fv(shadow_shader, "proj", proj); 
		shader_set_mat4fv(shadow_shader, "view", view);
		shader_set_3fv(shadow_shader, "light_pos", light_pos);
		shader_set_3fv(shadow_shader, "view_pos", state.camera.pos);
		shader_set_mat4fv(shadow_shader, "light_space_matrix", light_space_matrix); 
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, wood_texture);
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, depth_map);
		render_scene(shadow_shader, cube, plane);

		glUseProgram(color_shader);
		shader_set_mat4fv(shadow_shader, "proj", proj); 
		shader_set_mat4fv(shadow_shader, "view", view);
		model = mat4_make_scale(v3f(0.1f, 0.1f, 0.1f));
		model = mat4_translate(model, light_pos);
		shader_set_mat4fv(shadow_shader, "model", model);
		mesh_draw(cube);

		glDisable(GL_DEPTH_TEST);
		glUseProgram(debug_quad_shader);
		F32 scale = 0.3f;
		Transform transform = transform_make_scale_translate(v3f(scale, scale, scale),
								     v3f(1.0f-scale, 1.0f-scale, 0.0f));
		model = transform_apply(transform);
		shader_set_mat4fv(debug_quad_shader, "model", model);
		shader_set_1f(debug_quad_shader, "near", near);
		shader_set_1f(debug_quad_shader, "far", far);
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, depth_map);
		glBindVertexArray(quad_vao);
		glDrawArrays(GL_TRIANGLES, 0, 6);
		glBindVertexArray(0);
		glEnable(GL_DEPTH_TEST);
		glfwSwapBuffers(window);

		time = glfwGetTime();
		state.dt = time-last_time;
		last_time = time;
	}

	glfwTerminate();
	return(0);

error:
	glfwTerminate();
	return(1);
}