#include <stdio.h>
#include <stdlib.h>

#ifdef __APPLE__
#define GL_SILENCE_DEPRECATION
#endif

#include <OpenGL/gl3.h>
#include <GLFW/glfw3.h>
#ifndef __APPLE__
#define GLEW_STATIC
#include <GL/glew.h>
#endif

#include "config.h"
#include "scene.h"

#define STRINGIFY(X) QUOTESTRINGIFY(X)
#define QUOTESTRINGIFY(X) #X

#define GLFW_WINDOW_FRAMEBUFFER 0

GLint
compileprogram(const char *vertsrc, const char *fragsrc)
{
	GLint success;
	GLchar infolog[512];
	GLint program = glCreateProgram();
	if (program == 0) {
		exit(1);
	}
	
	GLuint vert = glCreateShader(GL_VERTEX_SHADER);
	if (vert == 0) {
		exit(1);
	}
	glShaderSource(vert, 1, &vertsrc, NULL);
	glCompileShader(vert);
	glGetShaderiv(vert, GL_COMPILE_STATUS, &success);
	if (success == GL_FALSE) {
		glGetShaderInfoLog(vert, sizeof(infolog), NULL, infolog);
		fputs(infolog, stderr);
		exit(1);
	}
	
	GLuint frag = glCreateShader(GL_FRAGMENT_SHADER);
	if (frag == 0) {
		exit(1);
	}
	glShaderSource(frag, 1, &fragsrc, NULL);
	glCompileShader(frag);
	glGetShaderiv(frag, GL_COMPILE_STATUS, &success);
	if (success == GL_FALSE) {
		glGetShaderInfoLog(frag, sizeof(infolog), NULL, infolog);
		fputs(infolog, stderr);
		exit(1);
	}
	
	glAttachShader(program, vert);
	glAttachShader(program, frag);
	glLinkProgram(program);
	glDeleteShader(vert);
	glDeleteShader(frag);
	glGetProgramiv(program, GL_LINK_STATUS, &success);
	if (success == GL_FALSE) {
		glGetProgramInfoLog(program, sizeof(infolog), NULL, infolog);
		fputs(infolog, stderr);
		exit(1);
	}
	glValidateProgram(program);
	glGetProgramiv(program, GL_VALIDATE_STATUS, &success);
	if (!success) {
		glGetProgramInfoLog(program, sizeof(infolog), NULL, infolog);
		fputs(infolog, stderr);
		exit(1);
	}
	return program;
}

#define GLSL_LINE(line) "#line "STRINGIFY(line)"\n\n" // Extra \n to line up GLSL with the current file.

const char *vertsrc =
	"#version 410 core\n"
	GLSL_LINE(__LINE__)
	"void\n"
	"main()\n"
	"{\n"
	"    // (-1,-1), (-1,1), (1,-1), (1,1)\n"
	"    gl_Position = vec4(2*(gl_VertexID/2) - 1, 2*(gl_VertexID%2) - 1, 0, 1);\n"
	"}\n";

const char *fragsrc =
	"#version 410 core\n"
	GLSL_LINE(__LINE__)
	"out vec4 fragcolor;\n"
	"\n"
	"uniform float time;\n"
	"uniform mat4 transform;\n"
	"uniform vec3 positionminbound;\n"
	"uniform vec3 positionmaxbound;\n"
	"uniform sampler2D positionmap;\n"
	"uniform sampler2D lightmap;\n"
	"\n"
	"const vec2 resolution = vec2("STRINGIFY(WIDTH)", "STRINGIFY(HEIGHT)");\n"
	"const float a = resolution.x/resolution.y;\n"
	"const float near = 0.5; // TODO: Unhardcode the near value.\n"
	"\n"
	"struct Hitinfo {\n"
	"    bool hit;\n"
	"    float dst;\n"
	"    vec3 pos;\n"
	"    vec3 normal;\n"
	"};\n"
	"\n"
	"struct Ray {\n"
	"    vec3 origin;\n"
	"    vec3 dir;\n"
	"};\n"
	"\n"
	"Hitinfo\n"
	"raysphere(Ray ray, vec3 center, float radius)\n"
	"{\n"
	"    Hitinfo info;\n"
	"    info.hit = false;\n"
	"    vec3 offsetorigin = ray.origin - center;\n"
	"    float a = dot(ray.dir, ray.dir);\n"
	"    float b = 2 * dot(offsetorigin, ray.dir);\n"
	"    float c = dot(offsetorigin, offsetorigin) - radius*radius;\n"
	"    float discriminant = b*b - 4*a*c;\n"
	"    \n"
	"    if (discriminant >= 0) {\n"
	"        float dst = (-b - sqrt(discriminant)) / (2*a);\n"
	"        \n"
	"        if (dst >= 0) {\n"
	"            info.hit = true;\n"
	"            info.dst = dst;\n"
	"            info.pos = ray.origin + ray.dir*dst;\n"
	"            info.normal = normalize(info.pos - center);\n"
	"        }\n"
	"    }\n"
	"    return info;\n"
	"}\n"
	"\n"
	"void\n"
	"main()\n"
	"{\n"
	"    mat4 local2world = transform;\n"
	"    mat4 world2local = inverse(local2world);\n"
	"    vec3 camerapos = transform[3].xyz;\n"
	"    \n"
	"    vec2 uv = gl_FragCoord.xy/resolution;\n"
	"    vec3 screenlocal = vec3(\n"
	"        uv.x - 0.5,\n"
	"        (uv.y - 0.5)/a,\n"
	"        near\n"
	"    );\n"
	"    vec3 screenworld = (local2world*vec4(screenlocal, 1)).xyz;\n"
	"    vec3 screendir = normalize(screenworld - camerapos);\n"
	"    \n"
	"    Hitinfo movingball = raysphere(\n"
	"        Ray(camerapos, screendir),\n"
	"        vec3(sin(time*2), 0.5, cos(time*2)),\n"
	"        0.1\n"
	"    );\n"
	"    if (movingball.hit) {\n"
	"        vec3 fragpos = mix(positionminbound, positionmaxbound, texture(positionmap, uv).xyz);\n"
	"        vec3 hitlocalpos = (world2local*vec4(movingball.pos, 1)).xyz;\n"
	"        if (hitlocalpos.z > fragpos.z) {\n"
	"            fragcolor = vec4(vec3(texture(lightmap, uv).r), 1);\n"
	"        } else {\n"
	"            fragcolor = vec4(vec3(0), 1);\n"
	"        }\n"
	"    } else {\n"
	"        fragcolor = vec4(vec3(texture(lightmap, uv).r), 1);\n"
	"    }\n"
	"}\n";

struct {
	GLint handle;
	struct {
		GLint time;
		GLint transform;
		GLint positionminbound;
		GLint positionmaxbound;
		GLint positionmap;
		GLint lightmap;
	} uniforms;
} sceneprogram;

void
compilesceneprogram(void)
{
	GLint program = compileprogram(vertsrc, fragsrc);
	glUseProgram(program);
	{
		sceneprogram.uniforms.time = glGetUniformLocation(program, "time");
		sceneprogram.uniforms.transform = glGetUniformLocation(program, "transform");
		sceneprogram.uniforms.positionminbound = glGetUniformLocation(program, "positionminbound");
		sceneprogram.uniforms.positionmaxbound = glGetUniformLocation(program, "positionmaxbound");
		sceneprogram.uniforms.positionmap = glGetUniformLocation(program, "positionmap");
		sceneprogram.uniforms.lightmap = glGetUniformLocation(program, "lightmap");
	}
	sceneprogram.handle = program;
}

static GLuint sceneframebuffer = 0;

void
initsceneframebuffer(void)
{
	GLuint texture;
	glGenTextures(1, &texture);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture);
	{
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, WIDTH, HEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
	}
	
	glGenFramebuffers(1, &sceneframebuffer);
	glBindFramebuffer(GL_FRAMEBUFFER, sceneframebuffer);
	{
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
		
		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
			fprintf(stderr, "ERROR: could not initialize the scene frame buffer\n");
			exit(1);
		}
	}
}

static GLuint positionmaptexture = 0;

void
reloadpositionmaptexture(void)
{
	glDeleteTextures(1, &positionmaptexture);
	glGenTextures(1, &positionmaptexture);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, positionmaptexture);
	{
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, positionmap);
	}
}

static GLuint lightmaptexture = 0;

void
reloadlightmaptexture(void)
{
	glDeleteTextures(1, &lightmaptexture);
	glGenTextures(1, &lightmaptexture);
	glActiveTexture(GL_TEXTURE2);
	glBindTexture(GL_TEXTURE_2D, lightmaptexture);
	{
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, WIDTH, HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, lightmap);
	}
}

void
reloadtextures(void)
{
	reloadpositionmaptexture();
	reloadlightmaptexture();
}

void
errorcallback(int error, const char *description)
{
	(void) error;
	fputs(description, stderr);
}

void
keycallback(GLFWwindow *window, int key, int scancode, int action, int mods)
{
	(void) window;
	(void) key;
	(void) scancode;
	(void) action;
	(void) mods;
}

void
framebuffersizecallback(GLFWwindow *window, int width, int height)
{
	(void) window;
	glViewport(0, 0, width, height);
}

int
main(void)
{
	glfwSetErrorCallback(errorcallback);
	
	if (!glfwInit()) {
		exit(1);
	}
	
#ifndef __APPLE__
	if (glewInit() != GLEW_OK) {
		exit(1);
	}
#endif
	
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	
	GLFWwindow *window = glfwCreateWindow(WIDTH, HEIGHT, "OpenGL", NULL, NULL);
	if (window == NULL) {
		glfwTerminate();
		exit(1);
	}
	
	glfwSetKeyCallback(window, keycallback);
	glfwSetFramebufferSizeCallback(window, framebuffersizecallback);
	
	glfwMakeContextCurrent(window);
	glfwSwapInterval(1); // Used to avoid screen tearing.
	
	GLuint vao;
	glGenVertexArrays(1, &vao);
	glBindVertexArray(vao);
	{
		compilesceneprogram();
		initsceneframebuffer();
		reloadtextures();
		
		while (!glfwWindowShouldClose(window)) {
			// Draw the scene to the scene frame buffer.
			glBindFramebuffer(GL_FRAMEBUFFER, sceneframebuffer);
			{
				glClear(GL_COLOR_BUFFER_BIT);
				glUseProgram(sceneprogram.handle);
				{
					double time = glfwGetTime();
					glUniform1f(sceneprogram.uniforms.time, time);
					glUniformMatrix4fv(sceneprogram.uniforms.transform, 1, GL_FALSE, (const GLfloat *)&transform);
					glUniform1i(sceneprogram.uniforms.positionmap, 1);
					glUniform1i(sceneprogram.uniforms.lightmap, 2);
					glUniform3fv(sceneprogram.uniforms.positionminbound, 1, (const GLfloat *)&positionminbound);
					glUniform3fv(sceneprogram.uniforms.positionmaxbound, 1, (const GLfloat *)&positionmaxbound);
					
					glActiveTexture(GL_TEXTURE1);
					glBindTexture(GL_TEXTURE_2D, positionmaptexture);
					glActiveTexture(GL_TEXTURE2);
					glBindTexture(GL_TEXTURE_2D, lightmaptexture);
					{
						glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
					}
				}
			}
			
			// Copy the scene frame buffer to the window frame buffer.
			glBindFramebuffer(GL_READ_FRAMEBUFFER, sceneframebuffer);
			glBindFramebuffer(GL_DRAW_FRAMEBUFFER, GLFW_WINDOW_FRAMEBUFFER);
			{
				int width, height;
				glfwGetFramebufferSize(window, &width, &height);
				
				glClear(GL_COLOR_BUFFER_BIT);
				// TODO: Apply the pixel-perfect scaling, width-first, crop the height if necessary.
				glBlitFramebuffer(0, 0, WIDTH, HEIGHT, 0, 0, width, height, GL_COLOR_BUFFER_BIT, GL_NEAREST);
			}
			
			// TODO: Limit FPS.
			glfwSwapBuffers(window);
			glfwPollEvents();
		}
	}
	glBindVertexArray(0);
	
	glfwDestroyWindow(window);
	glfwTerminate();
}
