#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <time.h>
#include <unistd.h>

#define GL_GLEXT_PROTOTYPES 1
#ifdef USE_OSM
#include <GL/glu.h>
#include <GL/osmesa.h>
#else
#include <GL/gl.h>
#include <EGL/egl.h>
#endif

#include <png.h>

#include "global.h"
#include "linmath.h"
#include "resources.h"
#include "planet.h"
#include "avali_system.h"
#include "skybox.h"
#include "terrain.h"

void screenshot(char* filename) {
	png_infop info_ptr = NULL;
	png_structp png_ptr = NULL;
	png_byte** row_pointers = NULL;
	int x,y;
	uint8_t* pixels = (uint8_t*)malloc(W_WIDTH * W_HEIGHT * 3);
	assert(pixels != 0);
	glReadPixels(0, 0, W_WIDTH, W_HEIGHT, GL_RGB, GL_UNSIGNED_BYTE, pixels);
	FILE* fp = fopen(filename, "wb");
	if(!fp) goto screenshot_failed;
	png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
	if(!png_ptr) goto screenshot_failed;
	info_ptr = png_create_info_struct(png_ptr);
	if(!info_ptr) goto screenshot_failed;
	if(setjmp(png_jmpbuf(png_ptr))) goto screenshot_failed;
	
	png_set_IHDR(png_ptr, info_ptr, W_WIDTH, W_HEIGHT, 8, PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
	
	uint8_t * pixel_ptr = pixels;
	row_pointers = png_malloc(png_ptr, W_HEIGHT * sizeof(png_byte *));
	for(y = 0; y < W_HEIGHT; y++) {
		png_byte *row = png_malloc(png_ptr, sizeof(uint8_t) * W_WIDTH * 3);
		row_pointers[y] = row;
		for(x = 0; x < W_WIDTH; x++) {
			*row++ = *pixel_ptr++;
			*row++ = *pixel_ptr++;
			*row++ = *pixel_ptr++;
		}
	}
	
	png_init_io(png_ptr, fp);
	png_set_rows(png_ptr, info_ptr, row_pointers);
	png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
	for(y = 0; y < W_HEIGHT; y++) png_free(png_ptr, row_pointers[y]);
	png_free(png_ptr, row_pointers);
	free(pixels);
	png_destroy_write_struct(&png_ptr, &info_ptr);
	fclose(fp);
	return;
screenshot_failed:
	free(pixels);
	png_destroy_write_struct(&png_ptr, &info_ptr);
	fclose(fp);
	printf("Something went wrong with libpng\r\n");
	return;
}

#ifdef USE_OSM
OSMesaContext ctx;
GLubyte *osm_buffer;
#endif
void create_egl_context() {
#ifdef USE_OSM
	ctx = OSMesaCreateContextExt(OSMESA_RGBA, 16, 0, 0, NULL);
	assert(ctx);
	osm_buffer = (GLubyte*)malloc(W_WIDTH * W_HEIGHT * 4 * sizeof(GLubyte));
	assert(osm_buffer != 0);
	assert(OSMesaMakeCurrent(ctx, osm_buffer, GL_UNSIGNED_BYTE, W_WIDTH, W_HEIGHT));
#else
	EGLDisplay eglDpy = eglGetDisplay(EGL_DEFAULT_DISPLAY);
	EGLint major, minor;
	eglInitialize(eglDpy, &major, &minor);
	EGLint numConfigs;
	EGLConfig eglCfg;
	eglChooseConfig(eglDpy, configAttribs, &eglCfg, 1, &numConfigs);
	EGLSurface eglSurf = eglCreatePbufferSurface(eglDpy, eglCfg, pbufferAttribs);
	eglBindAPI(EGL_OPENGL_API);
	EGLContext context = eglCreateContext(eglDpy, eglCfg, EGL_NO_CONTEXT, NULL);
	eglMakeCurrent(eglDpy, eglSurf, eglSurf, context);
#endif
}

void common_ogl_setup() {
   printf("GL_RENDERER   = %s\n", (char *)glGetString(GL_RENDERER));
   printf("GL_VERSION    = %s\n", (char *)glGetString(GL_VERSION));
   printf("GL_VENDOR     = %s\n", (char *)glGetString(GL_VENDOR));
   printf("GL_SHADING_LANGUAGE_VERSION = %s\n", (char *)glGetString(GL_SHADING_LANGUAGE_VERSION));
   GLint value;
   glGetIntegerv(GL_MAX_TEXTURE_SIZE, &value);
   printf("GL_MAX_TEXTURE_SIZE = %d\r\n", value);
   glGetIntegerv(GL_MAX_CUBE_MAP_TEXTURE_SIZE, &value);
   printf("GL_MAX_CUBE_MAP_TEXTURE_SIZE = %d\r\n", value);
   //printf("GL_EXTENSIONS = %s\n", (char *)glGetString(GL_EXTENSIONS));
	resource_handler_reset();
	glEnable(GL_TEXTURE_2D);
	glClearColor(0.8,0,0.6,1);
	glPixelStorei(GL_PACK_ALIGNMENT, 1);
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}

camera cam;
mat4x4 VIM;
mat4x4 TFM;
GLuint fbo;
GLuint outputShaderProgram;
GLuint outputVAO;
texture cubeTextures[6];
GLuint cubeRenderbuffer;
void render_scene() {
	glEnable(GL_DEPTH_TEST);
	skybox_load_moon_data(valaya, solakku);
	
	glBindFramebuffer(GL_FRAMEBUFFER, fbo);
	glBindRenderbuffer(GL_RENDERBUFFER, cubeRenderbuffer);
	glFramebufferRenderbuffer(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, cubeRenderbuffer);
	glViewport(0, 0, W_WIDTH, W_WIDTH);
	for(int i = 0; i < 6; i++) {
		printf("%d ", i + 1);
		fflush(stdout);
		cam.roll = 0;
		cam.x = 0;
		cam.z = 0;
		cam.y = 1000;
		switch(i) {
			default:
			case 1:
				cam.pitch = 0;
				cam.yaw = 90+90;
				break;
			case 0:
				cam.pitch = 0;
				cam.yaw = -90+90;
				break;
			case 3:
				cam.pitch = -90;
				cam.yaw = 180+90;
				break;
			case 2:
				cam.pitch = 90;
				cam.yaw = 180+90;
				break;
			case 4:
				cam.pitch = 0;
				cam.yaw = 180+90;
				break;
			case 5:
				cam.pitch = 0;
				cam.yaw = 0+90;
				break;
		}
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, cubeTextures[i].id, 0);
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
		skybox_render(cam);
		//Dirty hack. The terrain heightmap doesn’t tile. But its just for those distant peaks. You won’t be able to see the seams.
		for(int i = 0; i < 3; i++) {
			for(int j = 0; j < 3; j++) {
				terrain_render(cam, curr_sunlight_dir, i - 1, j - 1);
			}
		}
	}
	printf("\r\n");
	glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindRenderbuffer(GL_RENDERBUFFER, 0);
	glViewport(0, 0, W_WIDTH, W_HEIGHT);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glDisable(GL_DEPTH_TEST);
	glBindVertexArray(outputVAO);
	glEnableVertexAttribArray(0);
	glUseProgram(outputShaderProgram);
	
	for(int i = 0; i < 6; i++) {
		glActiveTexture(GL_TEXTURE0 + i);
		glBindTexture(GL_TEXTURE_2D, cubeTextures[i].id);
	}
	printf("Stitching...");
	fflush(stdout);
	glDrawArrays(GL_TRIANGLES, 0, 6);
	printf("Done.\r\n");
	glDisableVertexAttribArray(0);
	glBindVertexArray(0);
}

int main(int argc, char **argv) {
	create_egl_context();
	common_ogl_setup();
	init_avali_system();
	{
		double initial_time = 1705438607647.0 / 1000.0 + 32 * 60 * 60;
		update_avali_system(initial_time);
		printf("Initial time: %f\r\n", initial_time);
	}
	cam.x = cam.y = cam.z = 0;
	cam.pitch = cam.yaw = cam.roll = 0;

	/*
	 * Projection Matrix
	 */
	mat4x4 PJM;
	mat4x4_perspective(PJM, 90 * DEG2RAD, 1.0f, 0.3f, 25000.0f);
	printf("Projection Matrix:\r\n");
	int i,j;
	for(i=0; i<4; ++i) {
		for(j=0; j<4; ++j)
			printf("%f, ", PJM[i][j]);
		printf("\r\n");
	}
	printf("\r\n");
	
	{
		skybox_setup(PJM);
		float quad_vertices[] = {
			// first triangle
			 1.0f,  1.0f, 0.0f,  // top right
			 1.0f, -1.0f, 0.0f,  // bottom right
			-1.0f,  1.0f, 0.0f,  // top left 
			// second triangle
			 1.0f, -1.0f, 0.0f,  // bottom right
			-1.0f, -1.0f, 0.0f,  // bottom left
			-1.0f,  1.0f, 0.0f   // top left
		};
		for(int i = 0; i < 6; i++) {
			cubeTextures[i] = blank_texture(W_WIDTH, W_WIDTH, 0);
			assert(cubeTextures[i].id != 0);
		}
		glGenRenderbuffers(1, &cubeRenderbuffer);
		glGenFramebuffers(1, &fbo);
		glBindRenderbuffer(GL_RENDERBUFFER, cubeRenderbuffer);
		glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, W_WIDTH, W_WIDTH);
		glBindRenderbuffer(GL_RENDERBUFFER, 0);
		outputShaderProgram = create_shader_program("output_vertex.glsl", "output_fragment.glsl");
		assert(outputShaderProgram != 0);
		outputVAO = create_vao();
		assert(outputVAO != 0);
		glBindVertexArray(outputVAO);
		GLuint vertex_VBO = create_vbo();
		assert(vertex_VBO != 0);
		glBindBuffer(GL_ARRAY_BUFFER, vertex_VBO);
		glBufferData(GL_ARRAY_BUFFER, sizeof(quad_vertices), quad_vertices, GL_STATIC_DRAW);
		glVertexAttribPointer(glGetAttribLocation(outputShaderProgram, "aPos"), 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), NULL);
		glBindVertexArray(0);
		glUseProgram(outputShaderProgram);
		glUniform2f(glGetUniformLocation(outputShaderProgram, "u_resolution"), W_WIDTH, W_HEIGHT);
		glUniform1i(glGetUniformLocation(outputShaderProgram, "posX"), 0);
		glUniform1i(glGetUniformLocation(outputShaderProgram, "negX"), 1);
		glUniform1i(glGetUniformLocation(outputShaderProgram, "posY"), 2);
		glUniform1i(glGetUniformLocation(outputShaderProgram, "negY"), 3);
		glUniform1i(glGetUniformLocation(outputShaderProgram, "posZ"), 4);
		glUniform1i(glGetUniformLocation(outputShaderProgram, "negZ"), 5);
	}
	/*
	 * Load scene stuff
	 */
	terrain_load_textures();
	terrain_build_mesh(15000, 1000, PJM, 4000);
	
	/*
	 * Render 6 sides of cubemap
	 */
	cam.roll = 0;
	cam.pitch = 0;
	cam.yaw = 0;
	cam.x = cam.y = cam.z = 0;
	
	char numBuff[64];
	struct timespec tim;
	char ctrlIn = 0;
	while(1) {
		if(read(STDIN_FILENO, &ctrlIn, 1) > 0) {
			if(ctrlIn == 'r') {
				render_scene();
				screenshot("output.png");
			}else if(ctrlIn == 'e') break;
			else if(ctrlIn == 's') {
				uint8_t ctr = 0;
				while(ctr < 64) {
					while(read(STDIN_FILENO, &ctrlIn, 1) == 0);
					numBuff[ctr] = ctrlIn;
					if(ctrlIn == ';') {
						numBuff[ctr] = 0;
						break;
					}
					ctr++;
				}
				double parsed = 0;
				sscanf(numBuff, "%lf", &parsed);
				if(parsed != 0) {
					update_avali_system(parsed);
					printf("\r\nTime changed.\r\n");
				}else printf("\r\nInvalid value.\r\n");
			}
		}
		tim.tv_sec = 0;
		tim.tv_nsec = 1000000;
		nanosleep(&tim, NULL);
	}
	
	glDeleteRenderbuffers(1, &cubeRenderbuffer);
	glDeleteFramebuffers(1, &fbo);
	cleanup();
#ifdef USE_OSM
	OSMesaDestroyContext(ctx);
	free(osm_buffer);
#endif
	return 0;
}
