#include <GL/glew.h>
#include <GL/freeglut.h>

#include <glm/mat4x4.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

#include <webp/decode.h>

#include <fmt/core.h>

#include <iostream>
#include <chrono>

#include <graphics.hpp>
#include <modelpart.hpp>
#include <model.hpp>
#include <paths.hpp>
#include <config.hpp>
#include <cv.hpp>
#include <args.hpp>
#include <error.hpp>

GLuint shader;	//standard shader program used for all elements
GLuint transUniform;	//location of the "transMatrix" transformation matrix uniform in the shader

float windowAspectRatio;

int frameCount;
std::chrono::high_resolution_clock::time_point secondStart;

//parts of the model (see modelpart.hpp)
Model* model;

void display () {
	glClear(GL_COLOR_BUFFER_BIT);
	windowAspectRatio =  glutGet(GLUT_WINDOW_WIDTH) / (float)glutGet(GLUT_WINDOW_HEIGHT);

	model->draw();

	glutSwapBuffers();
}

void initBuffers (GLuint* vaoNum) {
	//TODO: put quad stuff in header file or something
	GLfloat quad[] = {
		//vertex	UV/texcoord
		0.5f, 0.5f,	1.0f, 0.0f,
		0.5f, -0.5f,	1.0f, 1.0f,
		-0.5f, -0.5f,	0.0f, 1.0f,
		-0.5f, 0.5f,	0.0f, 0.0f,
	};

	GLuint quadElements[] = {
		0, 1, 2,
		2, 3, 0
	};

	glGenVertexArrays(1, vaoNum);
	glBindVertexArray(*vaoNum);

	GLuint vbo;	//vertex buffer
	glGenBuffers(1, &vbo);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);
	//change to GL_DYNAMIC_DRAW when using deformable meshes
	glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);

	GLuint ebo;	//element buffer
	glGenBuffers(1, &ebo);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(quadElements),  quadElements, GL_STATIC_DRAW);

	//tell OpenGL what to put in the input vars
	GLuint posAttr = glGetAttribLocation(shader, "position");
	glVertexAttribPointer(posAttr, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), 0);
	glEnableVertexAttribArray(posAttr);
	GLuint uvAttr = glGetAttribLocation(shader, "texcoord");
	glVertexAttribPointer(uvAttr, 2, GL_FLOAT, GL_FALSE, 4*sizeof(GLfloat), (void*)(2*sizeof(GLfloat)));
	glEnableVertexAttribArray(uvAttr);
}

void fpsCounterReset() {
	glutSetWindowTitle(fmt::format("{0} - {1} @ {2} FPS", PROJECT_NAME, model->getName(), frameCount).c_str());

	secondStart = std::chrono::high_resolution_clock::now();
	frameCount = 0;
}

void initGraphics () {
	int argc = 1;
	char *argv[1] = {(char*)"fc2d"};

	glutInit(&argc, argv);
	glutCreateWindow(PROJECT_NAME);
	glutInitWindowSize(512, 512);

	glewExperimental = GL_TRUE;
	glewInit();

	initShader();

	model = new Model(resolvePath(("models/"+optData.model+".fma").c_str()).c_str());

	fpsCounterReset();

	//enable blending for alpha textures
	glEnable(GL_BLEND);
	glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	//set background color
	glClearColor(0.5f, 0.5f, 0.5f, 1.0f);

	glutDisplayFunc(display);


	std::cout << "graphics init complete" << std::endl;
}

void initTexture (GLuint* texNum, unsigned char* buffer, size_t bufferLength) {
	glGenTextures(1, texNum);
	glBindTexture(GL_TEXTURE_2D, *texNum);

	int x, y, channels;
	GLubyte* pixels;
	int webp = WebPGetInfo(buffer, bufferLength, &x, &y);
	if (webp) {
		pixels = WebPDecodeRGBA(buffer, bufferLength, &x, &y);
	} else {
		//try stb_image (png, jpg, gif, etc)
		pixels = stbi_load_from_memory(buffer, bufferLength, &x, &y, &channels, 4);
		if (!pixels) {
			showError("Corrupt or unsupported texture format!", "Could not load texture", false);

			GLubyte defaultPixels[] =
				{255,   0, 255, 255, 	  0,   0,   0, 255,
				   0,   0,   0, 255, 	255,   0, 255, 255 };

			pixels = defaultPixels;
			x = 2;
			y = 2;
		}
	}

	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, x, y, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
}

void initShader() {
	const char* vsSrc = 
		"#version 120\n"
		"attribute vec2 position;"
		"attribute vec2 texcoord;"
		"varying vec2 uv;"
		"uniform mat4 transMatrix;"
		"void main () {"
		"gl_Position = transMatrix * vec4(position, 0.0, 1.0);"
		"uv = texcoord;"
		"}";

	const char* fsSrc = 
		"#version 120\n"
		"varying vec2 uv;"
		"uniform sampler2D tex;"
		"void main () {"
		"gl_FragColor = texture2D(tex, uv);"
		"}";

	//compile vert shader
	GLuint vs = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vs, 1, &vsSrc, NULL);
	glCompileShader(vs);
	GLint status;
	glGetShaderiv(vs, GL_COMPILE_STATUS, &status);
	if (status != GL_TRUE) { std::cout << "vertex shader borked" << std::endl;
				 printShaderCompileLog(vs); }

	//compile frag shader
	GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fs, 1, &fsSrc, NULL);
	glCompileShader(fs);
	glGetShaderiv(fs, GL_COMPILE_STATUS, &status);
	if (status != GL_TRUE) { std::cout << "fragment shader borked" << std::endl;
				 printShaderCompileLog(vs); }
	
	//link shaders into a shader program
	shader = glCreateProgram();
	glAttachShader(shader, vs);
	glAttachShader(shader, fs);
	//glBindFragDataLocation(shader, 0, "color");
	glLinkProgram(shader);
	glUseProgram(shader);

	//set an identity ("do nothing") transformation matrix as default
	transUniform = glGetUniformLocation(shader, "transMatrix");
	glUniformMatrix4fv(transUniform, 1, GL_FALSE, glm::value_ptr(glm::mat4(1.0f)));
}

void graphicsFrame () {
	glutMainLoopEvent();

	auto timeSinceSecondStart = std::chrono::high_resolution_clock::now() - secondStart;
	frameCount++;
	long usSinceSecondStart = std::chrono::duration_cast<std::chrono::microseconds>(timeSinceSecondStart).count();

	if (usSinceSecondStart >= 1000000) {
		fpsCounterReset();
	}
}

void printShaderCompileLog(GLuint shader) {
	char logBuffer[1024];
	glGetShaderInfoLog(shader, 1024, NULL, logBuffer);
	std::cout << "Compile log for shader " << shader << std::endl;
	std::cout << logBuffer << std::endl;
}

void updateModel(struct FaceData faceData) {
	/*
	//calculate transforms
	parts[0].setTransform(headPos, rotation, scale);
	parts[1].setTransform(facePos, rotation, scale);
	parts[2].setTransform(facePos, rotation, scale);

	//set mouth texture to open or closed
	parts[2].selectTexture(mouthOpen ? 1 : 0);
	*/
	model->updateTransforms(faceData);

	//tell FreeGLUT to schedule a screen update
	glutPostRedisplay();
}
