/* This file is part of ToaruOS and is released under the terms
 * of the NCSA / University of Illinois License - see LICENSE.md
 * Copyright (C) 2013-2014 Kevin Lange
 *
 * GL teapot with shaders.
 */
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <unistd.h>

#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/osmesa.h>
#include <GL/glext.h>

#include "lib/yutani.h"
#include "lib/graphics.h"
#include "lib/pthread.h"

#define PI  3.141592654
#define TAO 6.28318531

/* Despite including the correct header, these are not defined */
GLuint glCreateShader(GLenum shaderType);
void glCompileShader(GLuint shader);
void glAttachShader(GLuint  program,  GLuint  shader);
void glLinkProgram(GLuint program);
void glUseProgram(GLuint program);
GLuint glCreateProgram(void);
GLint glGetUniformLocation(GLuint  program,  const GLchar * name);
void glUniform1i(GLint location, GLint v0);
void glShaderSource(GLuint  shader,  GLsizei  count,  GLchar ** string,  GLint * length);

GLuint texture_a; /* Diffuse texture */
GLuint texture_b; /* Environment spheremap */

int quit = 0;

/* Scene scale */
float scale = 1.0;
/* Object rotation */
float rot = 0.0;
/* Camera height */
float height = 1.0;
/* Where to point the camera */
float cam_offset = 1.0;

char rotation_paused = 0;
int win_width;
int win_height;
float x_light;
float y_light;

/* Normal vector definition */
typedef struct {
	float x;
	float y;
	float z;
} normal_t ;

/* Vertex object */
typedef struct {
	float x; /* Coordinates */
	float y;
	float z;
	float u; /* Texture coordinates */
	float v;
	normal_t normal;
} vertex_t ;

/* Resizable array of vertices */
typedef struct {
	uint32_t len;
	uint32_t capacity;
	vertex_t ** nodes;
} vertices_t;

/* Face definition (3 vertices and a normal vector) */
typedef struct {
	vertex_t * a;
	vertex_t * b;
	vertex_t * c;
	normal_t normal;
} face_t;

/* Resizable array of faces */
typedef struct {
	uint32_t len;
	uint32_t capacity;
	face_t ** nodes;
} faces_t;

/* Model vertices */
vertices_t vertices = {.len = 0, .capacity = 0};
/* Model triangles */
faces_t    faces    = {.len = 0, .capacity = 0};

/* Initialize the model objects */
void init_model() {
	/* We give an initial capacity of 16 for each */
	vertices.capacity = 16;
	vertices.nodes    = (vertex_t **)malloc(sizeof(vertex_t *) * vertices.capacity);
	faces.capacity    = 16;
	faces.nodes       = (face_t **)malloc(sizeof(face_t *) * faces.capacity);
}

/* Add the given coordinates to the vertex list */
void add_vertex(float x, float y, float z) {
	if (vertices.len == vertices.capacity) {
		/* When we run out of space, increase by two */
		vertices.capacity *= 2;
		vertices.nodes = (vertex_t **)realloc(vertices.nodes, sizeof(vertex_t *) * vertices.capacity);
	}
	/* Create a new vertex in the list */
	vertices.nodes[vertices.len] = malloc(sizeof(vertex_t));
	vertices.nodes[vertices.len]->x = x * scale;
	vertices.nodes[vertices.len]->y = y * scale;
	vertices.nodes[vertices.len]->z = z * scale;
	/* Set texture coordinates by cylindrical mapping */
	float theta = atan2(z,x);
	vertices.nodes[vertices.len]->u = (theta + PI) / (TAO);
	vertices.nodes[vertices.len]->v = (y / 2.0);
	/* Initialize normals to 0,0,0 */
	vertices.nodes[vertices.len]->normal.x = 0.0f;
	vertices.nodes[vertices.len]->normal.y = 0.0f;
	vertices.nodes[vertices.len]->normal.z = 0.0f;
	vertices.len++;
}

/* Add a face with the given vertices */
void add_face(int a, int b, int c) {
	if (faces.len == faces.capacity) {
		/* Double size when we run out... */
		faces.capacity *= 2;
		faces.nodes = (face_t **)realloc(faces.nodes, sizeof(face_t *) * faces.capacity);
	}
	if (vertices.len < a || vertices.len < b || vertices.len < c) {
		/* Frick... */
		fprintf(stderr, "ERROR: Haven't yet collected enough vertices for the face %d %d %d (have %d)!\n", a, b, c, vertices.len);
		exit(1);
	}
	/* Create a new triangle */
	faces.nodes[faces.len] = malloc(sizeof(face_t));
	faces.nodes[faces.len]->a = vertices.nodes[a-1];
	faces.nodes[faces.len]->b = vertices.nodes[b-1];
	faces.nodes[faces.len]->c = vertices.nodes[c-1];
	/* Calculate some normals */
	vertex_t u = {.x = faces.nodes[faces.len]->b->x - faces.nodes[faces.len]->a->x,
				  .y = faces.nodes[faces.len]->b->y - faces.nodes[faces.len]->a->y,
				  .z = faces.nodes[faces.len]->b->z - faces.nodes[faces.len]->a->z};
	vertex_t v = {.x = faces.nodes[faces.len]->c->x - faces.nodes[faces.len]->a->x,
				  .y = faces.nodes[faces.len]->c->y - faces.nodes[faces.len]->a->y,
				  .z = faces.nodes[faces.len]->c->z - faces.nodes[faces.len]->a->z};
	/* Set the face normals */
	faces.nodes[faces.len]->normal.x = ((u.y * v.z) - (u.z * v.y));
	faces.nodes[faces.len]->normal.y = -((u.z * v.x) - (u.x * v.z));
	faces.nodes[faces.len]->normal.z = ((u.x * v.y) - (u.y * v.x));
	faces.len++;
}

void finish_normals() {
	/* Loop through vertices and accumulate normals for them */
	for (uint32_t i = 0; i < faces.len; ++i) {
		/* Vertex a */
		faces.nodes[i]->a->normal.x += faces.nodes[i]->normal.x;
		faces.nodes[i]->a->normal.y += faces.nodes[i]->normal.y;
		faces.nodes[i]->a->normal.z += faces.nodes[i]->normal.z;
		/* Vertex b */
		faces.nodes[i]->b->normal.x += faces.nodes[i]->normal.x;
		faces.nodes[i]->b->normal.y += faces.nodes[i]->normal.y;
		faces.nodes[i]->b->normal.z += faces.nodes[i]->normal.z;
		/* Vertex c */
		faces.nodes[i]->c->normal.x += faces.nodes[i]->normal.x;
		faces.nodes[i]->c->normal.y += faces.nodes[i]->normal.y;
		faces.nodes[i]->c->normal.z += faces.nodes[i]->normal.z;
	}
}

/* Discard the rest of this line */
void toss(FILE * f) {
	while (fgetc(f) != '\n');
}

/* Load a Wavefront Obj model */
void load_wavefront(char * filename) {
	/* Open the file */
	FILE * obj = fopen(filename, "r");
	int collected = 0;
	char d = ' ';
	/* Initialize the lists */
	init_model();
	while (!feof(obj)) {
		/* Scan in a line */
		collected = fscanf(obj, "%c ", &d);
		if (collected == 0) continue;
		switch (d) {
			case 'v':
				{
					/* Vertex */
					float x, y, z;
					collected = fscanf(obj, "%f %f %f\n", &x, &y, &z);
					if (collected < 3) fprintf(stderr, "ERROR: Only collected %d points!\n", collected);
					add_vertex(x, y, z);
				}
				break;
			case 'f':
				{
					/* Face */
					int a, b, c;
					collected = fscanf(obj, "%d %d %d\n", &a, &b, &c);
					if (collected < 3) fprintf(stderr, "ERROR: Only collected %d vertices!\n", collected);
					add_face(a,b,c);
				}
				break;
			default:
				/* Something else that we don't care about */
				toss(obj);
				break;
		}
	}
	/* Finalize the vertex normals */
	finish_normals();
	fclose(obj);
}

/* Vertex, fragment, program */
GLuint v, f, p;

/* Read a file into a buffer and return a pointer to the buffer */
char * readFile(char * filename, int32_t * size) {
	FILE * tex;
	char * texture;
	tex = fopen(filename, "r");
	fseek(tex, 0L, SEEK_END);
	*size = ftell(tex);
	texture = malloc(*size);
	fseek(tex, 0L, SEEK_SET);
	fread(texture, *size, 1, tex);
	fclose(tex);
	return texture;
}

/* Initialize the scene */
void init(char * object, char * diffuse, char * sphere) {
	load_wavefront(object);
	/* Check for GLEW compatibility */
#if 0
	glewInit();
	if (glewIsSupported("GL_VERSION_2_0")) {
		printf("Ready.\n");
	} else {
		/* We don't have OpenGL 2.0 support! BAIL! */
		printf("wtf?\n");
		exit(1);
	}
#endif

	/* Some nice defaults */
	glClearColor (0.0, 0.0, 0.0, 0.0);
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_TEXTURE_2D);

	/* Initialize the two textures */
	char * texture;
	int32_t size;
	int dif_size, env_size;
	glGenTextures(1,&texture_a);
	glGenTextures(1,&texture_b);
	/* Diffuse texture { */
	/* The diffuse texture is a wood texture */
	texture = readFile(diffuse, &size); /* We have stored are textures as raw RGBA */
	dif_size = (int)sqrt(size / 4);
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, texture_a);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
	glTexImage2D(GL_TEXTURE_2D, 0, 3, dif_size, dif_size, 0, GL_RGBA, GL_UNSIGNED_BYTE, texture);
	free(texture);
	/* } */
	/* Sphere map texture { */
	texture = readFile(sphere, &size);
	env_size = (int)sqrt(size / 4);
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, texture_b);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
	glTexImage2D(GL_TEXTURE_2D, 0, 3, env_size, env_size, 0, GL_RGBA, GL_UNSIGNED_BYTE, texture);
	free(texture);
	/* } */

	/* Load in the shader programs */
	char *vs = NULL,
		 *fs = NULL;
	int32_t v_size, f_size;
	v = glCreateShader(GL_VERTEX_SHADER);
	f = glCreateShader(GL_FRAGMENT_SHADER);
	vs = readFile("teapot.vert", &v_size); /* Vertex shader */
	fs = readFile("teapot.frag", &f_size); /* Fragment shader */
	glShaderSource(v, 1, &vs, (GLint *)&v_size); /* Load... */
	glShaderSource(f, 1, &fs, (GLint *)&f_size);
	free(vs); free(fs); /* Free the data blobs */
	glCompileShader(v); /* Compile... */
	glCompileShader(f);
	p = glCreateProgram(); /* Create a program */
	glAttachShader(p, v);  /* Attach the two shaders */
	glAttachShader(p, f);
	glLinkProgram(p);      /* Link it all together */

	/* Use our shaders */
	glUseProgram(p);

	/* Set the texture sources */
	GLint tex0 = glGetUniformLocation(p, "texture");
	GLint tex1 = glGetUniformLocation(p, "spheremap");
	glUniform1i(tex0, 0);
	glUniform1i(tex1, 1);

	/* Check for errors */
	GLenum glErr;
	int    retCode = 0;
	glErr = glGetError();
	while (glErr != GL_NO_ERROR)
	{
		//printf("glError: %s\n", gluErrorString(glErr));
		retCode = 1;
		glErr = glGetError();
	}


}

void lights(void) {
	/* Basic moving lighting */
	GLfloat white[] = {1.0,1.0,1.0,1.0};
	float l_scale = 7.0;
	GLfloat lpos[] = {l_scale * x_light, l_scale * y_light, 3.0};

	glEnable(GL_LIGHTING);
	glEnable(GL_LIGHT0);

	glLightfv(GL_LIGHT0, GL_POSITION, lpos);
	glLightfv(GL_LIGHT0, GL_AMBIENT, white);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, white);
	glLightfv(GL_LIGHT0, GL_SPECULAR, white);
}

void display(void) {
	glLoadIdentity ();
	lights();
	/* Point the camera */
	gluLookAt(4.0 * sin(rot),height,-4.0 * cos(rot),
			  0.0,cam_offset,0.0,
			  0.0,100.0,0.0);

	if (!rotation_paused) {
		rot += 0.002;
	}

	glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	/* Draw the teapot */
	glBegin(GL_TRIANGLES);
	for (uint32_t i = 0; i < faces.len; ++i) {
		glNormal3f(faces.nodes[i]->a->normal.x, faces.nodes[i]->a->normal.y, faces.nodes[i]->a->normal.z);
		glTexCoord2f(faces.nodes[i]->a->u,faces.nodes[i]->a->v);
		glVertex3f(faces.nodes[i]->a->x, faces.nodes[i]->a->y, faces.nodes[i]->a->z);
		glNormal3f(faces.nodes[i]->b->normal.x, faces.nodes[i]->b->normal.y, faces.nodes[i]->b->normal.z);
		glTexCoord2f(faces.nodes[i]->b->u,faces.nodes[i]->b->v);
		glVertex3f(faces.nodes[i]->b->x, faces.nodes[i]->b->y, faces.nodes[i]->b->z);
		glNormal3f(faces.nodes[i]->c->normal.x, faces.nodes[i]->c->normal.y, faces.nodes[i]->c->normal.z);
		glTexCoord2f(faces.nodes[i]->c->u,faces.nodes[i]->c->v);
		glVertex3f(faces.nodes[i]->c->x, faces.nodes[i]->c->y, faces.nodes[i]->c->z);
	}
	glEnd();

	glFlush ();

}

void reshape (int w, int h) {
	/* Reshape the viewport properly */
	win_width = w;
	win_height = h;
	glViewport (0, 0, (GLsizei) w, (GLsizei) h); 
	glMatrixMode (GL_PROJECTION);
	glLoadIdentity ();
	gluPerspective(90.0,(double)w / (double)h,0.0001,10.0);
	glMatrixMode (GL_MODELVIEW);
}

int resize(gfx_context_t * ctx, OSMesaContext gl_ctx) {

	if (!OSMesaMakeCurrent(gl_ctx, ctx->backbuffer, GL_UNSIGNED_BYTE, ctx->width, ctx->height))
		return 1;

	OSMesaPixelStore(OSMESA_Y_UP, 0);

	reshape(ctx->width, ctx->height);
	return 0;
}

void keyboard(unsigned char key, int x, int y)
{
	switch (key) {
		case 'w':
			/* Raise camera */
			height += 0.07;
			break;
		case 's':
			/* Lower camera */
			height -= 0.07;
			break;
		case 'p':
			/* Pause / unpause object movement */
			rotation_paused = !rotation_paused;
			break;
		case 'q':
			quit = 1;
			break;
	}
}

void mouse(int x, int y) {
	x_light = (x - (float)(win_width / 2)) / ((float)win_height) ;
	y_light = (y - (float)(win_height / 2)) / ((float)win_height);
}

static yutani_t * yctx;
static yutani_window_t * wina;
static gfx_context_t * ctx;

void * draw_thread(void * glctx) {
	while (!quit) {
		display();
		flip(ctx);
		yutani_flip(yctx, wina);
		syscall_yield();
	}

	pthread_exit(0);
}


int main(int argc, char** argv) {
	/* default values */
	char * filename = "teapot.obj";
	char * diffuse  = "wood.rgba";
	char * sphere   = "nvidia.rgba";
	int c, index;

	chdir("/opt/examples");

	/* Parse some command-line arguments */
	while ((c = getopt(argc, argv, "d:e:h:s:")) != -1) {
		switch (c) {
			case 'd':
				diffuse = optarg;
				break;
			case 'e':
				sphere = optarg;
				break;
			case 's':
				/* Set scale */
				scale = atof(optarg);
				break;
			case 'h':
				cam_offset = atof(optarg);
				break;
			default:
				/* Uh, that's it for -args */
				printf("Unrecognized argument!\n");
				break;
		}
	}
	/* Get an optional filename from the last non-- parameter */
	for (index = optind; index < argc; ++index) {
		filename = argv[index];
	}

	printf("Press q to exit.\n");

	yctx = yutani_init();
	wina = yutani_window_create(yctx, 500, 500);
	yutani_window_move(yctx, wina, 100, 100);
	ctx = init_graphics_yutani_double_buffer(wina);
	draw_fill(ctx, rgb(0,0,0));
	yutani_window_update_shape(yctx, wina, YUTANI_SHAPE_THRESHOLD_HALF);

	yutani_window_advertise_icon(yctx, wina, "GL Teapot", "teapot");

	OSMesaContext gl_ctx = OSMesaCreateContext(OSMESA_BGRA, NULL);
	if (resize(ctx, gl_ctx)) {
		fprintf(stderr, "%s: Something bad happened.\n", argv[0]);
		goto finish;
	}

	/* Load up the file, set everything else up */
	init (filename, diffuse, sphere);

	/* XXX add a method to query if there are available packets in pex */
	pthread_t thread;
	pthread_create(&thread, NULL, draw_thread, NULL);

	while (!quit) {
		yutani_msg_t * m = yutani_poll(yctx);
		if (m) {
			switch (m->type) {
				case YUTANI_MSG_KEY_EVENT:
					{
						struct yutani_msg_key_event * ke = (void*)m->data;
						if (ke->event.action == KEY_ACTION_DOWN) {
							keyboard(ke->event.keycode, 0, 0);
						}
					}
					break;
				case YUTANI_MSG_WINDOW_MOUSE_EVENT:
					{
						struct yutani_msg_window_mouse_event * me = (void*)m->data;
						if (me->command == YUTANI_MOUSE_EVENT_DOWN && me->buttons & YUTANI_MOUSE_BUTTON_LEFT) {
							yutani_window_drag_start(yctx, wina);
						}
					}
					break;
				case YUTANI_MSG_SESSION_END:
					quit = 1;
					break;
				default:
					break;
			}
			free(m);
		}
	}

finish:
	OSMesaDestroyContext(gl_ctx);
	yutani_close(yctx, wina);

	return 0;
}
