/*
  Copyright (c) 2016 Alex Diener
  
  This software is provided 'as-is', without any express or implied
  warranty. In no event will the authors be held liable for any damages
  arising from the use of this software.
  
  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it
  freely, subject to the following restrictions:
  
  1. The origin of this software must not be misrepresented; you must not
     claim that you wrote the original software. If you use this software
     in a product, an acknowledgment in the product documentation would be
     appreciated but is not required.
  2. Altered source versions must be plainly marked as such, and must not be
     misrepresented as being the original software.
  3. This notice may not be removed or altered from any source distribution.
  
  Alex Diener alex@ludobloom.com
*/

#include "gamemath/Matrix4x4f.h"
#include "gamemath/Quaternionf.h"
#include "gamemath/Vector3f.h"
#include "gamemath/VectorConversions.h"
#include "renderer/VertexTypes.h"
#include "shadercollection/ShaderCollection.h"
#include "shell/Shell.h"
#include "shell/ShellKeyCodes.h"
#include "testharness/Obj3DModelIO.h"
#include "testharness/SharedEvents.h"
#include "testharness/TestHarness_globals.h"
#include "testharness/TrimeshViewerScreen.h"
#include <limits.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

#define stemobject_implementation TrimeshViewerScreen

stemobject_vtable_begin();
stemobject_vtable_entry(dispose);
stemobject_vtable_entry(activate);
stemobject_vtable_entry(deactivate);
stemobject_vtable_end();

TrimeshViewerScreen * TrimeshViewerScreen_create(Renderer * renderer) {
	stemobject_create_implementation(init, renderer)
}

bool TrimeshViewerScreen_init(TrimeshViewerScreen * self, Renderer * renderer) {
	call_super(init, self);
	self->renderer = renderer;
	self->renderLayer = RenderLayer_create(RENDER_LAYER_SORT_NONE, NULL, NULL);
	self->shaderConfiguration = ShaderConfiguration3DLit_create(ShaderCollection_get3DLitShader());
	call_virtual(setTexture, self->shaderConfiguration, 0, g_blankTexture, false);
	self->renderPipelineConfiguration = default3DRenderPipelineConfiguration(RENDER_BLEND_NONE);
	self->triangleRenderable = Renderable_createWithVertexBuffer(PRIMITIVE_TRIANGLES,
	                                                             &self->renderPipelineConfiguration,
	                                                             self->shaderConfiguration,
	                                                             VertexBuffer_create(self->shaderConfiguration->shader->vertexFormat, 0, NULL, INDEX_SIZE_UINT32, 0, NULL, BUFFER_USAGE_STREAM),
	                                                             true,
	                                                             NULL,
	                                                             NULL);
	self->normalRenderable = Renderable_createWithVertexBuffer(PRIMITIVE_LINES,
	                                                           &self->renderPipelineConfiguration,
	                                                           self->shaderConfiguration,
	                                                           VertexBuffer_create(self->shaderConfiguration->shader->vertexFormat, 0, NULL, INDEX_SIZE_UINT32, 0, NULL, BUFFER_USAGE_STREAM),
	                                                           true,
	                                                           NULL,
	                                                           NULL);
	RenderLayer_addRenderable(self->renderLayer, self->triangleRenderable, 0, RECT4i_EMPTY);
	RenderLayer_addRenderable(self->renderLayer, self->normalRenderable, 0, RECT4i_EMPTY);
	return true;
}

void TrimeshViewerScreen_dispose(TrimeshViewerScreen * self) {
	if (self->trimesh != NULL) {
		CollisionStaticTrimesh_dispose(self->trimesh);
		self->trimesh = NULL;
	}
	Renderable_dispose(self->triangleRenderable);
	Renderable_dispose(self->normalRenderable);
	ShaderConfiguration3DLit_dispose(self->shaderConfiguration);
	RenderLayer_dispose(self->renderLayer);
	call_super(dispose, self);
}

static void writeTrimeshTriangleVertices(CollisionStaticTrimesh * trimesh, VertexIO * vertexIO) {
	struct vertex_p3f_t2f_n3f_c4f vertices[trimesh->triangleCount * 3], vertex = {.texCoords = {0.0f, 0.0f}, .color = {1.0f, 1.0f, 1.0f, 1.0f}};
	uint32_t indexes[trimesh->triangleCount * 3];
	Vector3f normal;
	Vector3f position;
	
	for (unsigned int triangleIndex = 0; triangleIndex < trimesh->triangleCount; triangleIndex++) {
		normal = Vector3x_toVector3f(trimesh->triangles[triangleIndex].normal);
		vertex.normal[0] = normal.x;
		vertex.normal[1] = normal.y;
		vertex.normal[2] = normal.z;
		position = Vector3x_toVector3f(trimesh->vertices[trimesh->triangles[triangleIndex].vertexIndexes[0]].position);
		vertex.position[0] = position.x;
		vertex.position[1] = position.y;
		vertex.position[2] = position.z;
		vertices[triangleIndex * 3 + 0] = vertex;
		position = Vector3x_toVector3f(trimesh->vertices[trimesh->triangles[triangleIndex].vertexIndexes[1]].position);
		vertex.position[0] = position.x;
		vertex.position[1] = position.y;
		vertex.position[2] = position.z;
		vertices[triangleIndex * 3 + 1] = vertex;
		position = Vector3x_toVector3f(trimesh->vertices[trimesh->triangles[triangleIndex].vertexIndexes[2]].position);
		vertex.position[0] = position.x;
		vertex.position[1] = position.y;
		vertex.position[2] = position.z;
		vertices[triangleIndex * 3 + 2] = vertex;
		
		indexes[triangleIndex * 3 + 0] = triangleIndex * 3 + 0;
		indexes[triangleIndex * 3 + 1] = triangleIndex * 3 + 1;
		indexes[triangleIndex * 3 + 2] = triangleIndex * 3 + 2;
	}
	VertexIO_writeIndexedVertices(vertexIO, sizeof(vertices) / sizeof(vertices[0]), vertices, sizeof(indexes) / sizeof(indexes[0]), indexes);
}

#define NORMAL_SCALE 0.25f

static void writeTrimeshNormalVertices(CollisionStaticTrimesh * trimesh, VertexIO * vertexIO) {
	struct vertex_p3f_t2f_n3f_c4f vertices[trimesh->vertexCount * 2], vertex = {.texCoords = {0.0f, 0.0f}, .color = {1.0f, 1.0f, 1.0f, 1.0f}};
	uint32_t indexes[trimesh->vertexCount * 2];
	Vector3f normal;
	Vector3f position;
	
	for (unsigned int vertexIndex = 0; vertexIndex < trimesh->vertexCount; vertexIndex++) {
		normal = Vector3x_toVector3f(trimesh->vertices[vertexIndex].normal);
		vertex.normal[0] = normal.x;
		vertex.normal[1] = normal.y;
		vertex.normal[2] = normal.z;
		position = Vector3x_toVector3f(trimesh->vertices[vertexIndex].position);
		vertex.position[0] = position.x;
		vertex.position[1] = position.y;
		vertex.position[2] = position.z;
		vertices[vertexIndex * 2 + 0] = vertex;
		vertex.position[0] += normal.x * NORMAL_SCALE;
		vertex.position[1] += normal.y * NORMAL_SCALE;
		vertex.position[2] += normal.z * NORMAL_SCALE;
		vertices[vertexIndex * 2 + 1] = vertex;
		
		indexes[vertexIndex * 2 + 0] = vertexIndex * 2 + 0;
		indexes[vertexIndex * 2 + 1] = vertexIndex * 2 + 1;
	}
	
	VertexIO_writeIndexedVertices(vertexIO, sizeof(vertices) / sizeof(vertices[0]), vertices, sizeof(indexes) / sizeof(indexes[0]), indexes);
}

static void initRenderablesWithTrimesh(TrimeshViewerScreen * self, CollisionStaticTrimesh * trimesh) {
	VertexIO * vertexIO = VertexIO_create(self->shaderConfiguration->shader->vertexFormat, 0.0, 0.0);
	writeTrimeshTriangleVertices(trimesh, vertexIO);
	VertexBuffer_bufferData(self->triangleRenderable->vertexBuffer, vertexIO->vertexCount, vertexIO->vertices, vertexIO->indexCount, vertexIO->indexes, BUFFER_USAGE_STREAM);
	VertexIO_dispose(vertexIO);
	
	vertexIO = VertexIO_create(self->shaderConfiguration->shader->vertexFormat, 0.0, 0.0);
	writeTrimeshNormalVertices(trimesh, vertexIO);
	VertexBuffer_bufferData(self->normalRenderable->vertexBuffer, vertexIO->vertexCount, vertexIO->vertices, vertexIO->indexCount, vertexIO->indexes, BUFFER_USAGE_STREAM);
}

static void loadTrimeshPreset1(TrimeshViewerScreen * self) {
	CollisionStaticTrimesh * trimesh;
	Vector3x vertices[] = {{0x01000, 0x01000, 0x01000}, {0x20000, 0x00000, 0x00000}, {0x00000, 0x00000, 0x20000},
	                       {0x01000, 0x01000, 0x01000}, {0x00000, 0x00000, 0x20000}, {0x00000, 0x20000, 0x00000},
	                       {0x01000, 0x01000, 0x01000}, {0x00000, 0x20000, 0x00000}, {0x20000, 0x00000, 0x00000},
	                       {0x00000, 0x20000, 0x00000}, {0x00000, 0x00000, 0x20000}, {0x20000, 0x00000, 0x00000}};
	trimesh = CollisionStaticTrimesh_create(NULL, NULL, NULL, vertices, 12);
	initRenderablesWithTrimesh(self, trimesh);
	CollisionStaticTrimesh_dispose(trimesh);
	Shell_redisplay();
}

static void loadTrimeshPreset2(TrimeshViewerScreen * self) {
	CollisionStaticTrimesh * trimesh;
	Vector3x vertices[] = {{0x00000, 0x00000, 0x00000}, {0x10000, 0x00000, 0x00000}, {0x00000, 0x10000, 0x00000},
	                       {0x00000, 0x10000, 0x00000}, {0x10000, 0x00000, 0x00000}, {0x10000, 0x10000, -0x10000},
	                       {0x10000, 0x00000, 0x00000}, {0x00000, 0x00000, 0x00000}, {0x10000, 0x10000, -0x10000}};
	trimesh = CollisionStaticTrimesh_create(NULL, NULL, NULL, vertices, 9);
	initRenderablesWithTrimesh(self, trimesh);
	CollisionStaticTrimesh_dispose(trimesh);
	Shell_redisplay();
}

static bool draw(Atom eventID, void * eventData, void * context) {
	TrimeshViewerScreen * self = context;
	Matrix4x4f matrix = MATRIX4x4f_IDENTITY;
	
	Matrix4x4f_translate(&matrix, V3f(0.0f, 0.0f, -xtof(self->cameraDistance)));
	Matrix4x4f_multiply(&matrix, Matrix4x4f_fromQuaternionf(Quaternionx_toQuaternionf(self->cameraDirection)));
	Matrix4x4f_translate(&matrix, V3f(xtof(self->cameraFocus.x), xtof(self->cameraFocus.y), xtof(self->cameraFocus.z)));
	call_virtual(setViewMatrix, self->shaderConfiguration, matrix);
	
	Renderer_clear(self->renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f));
	Renderer_drawLayer(self->renderer, self->renderLayer, 0.0, 0.0);
	
	return true;
}

static bool keyDown(Atom eventID, void * eventData, void * context) {
	TrimeshViewerScreen * self = context;
	struct keyEvent * event = eventData;
	
	switch (event->keyCode) {
		case KEY_CODE_O: {
			const char * filePath = Shell_openFileDialog(NULL, NULL, NULL, 0, NULL);
			if (filePath != NULL) {
				CollisionStaticTrimesh * trimesh = loadObjStaticModelFile(filePath);
				if (trimesh != NULL) {
					initRenderablesWithTrimesh(self, trimesh);
					CollisionStaticTrimesh_dispose(trimesh);
					Shell_redisplay();
				}
			}
			break;
		}
		case KEY_CODE_1:
			loadTrimeshPreset1(self);
			break;
			
		case KEY_CODE_2:
			loadTrimeshPreset2(self);
			break;
	}
	return false;
}

static bool mouseDown(Atom eventID, void * eventData, void * context) {
	Shell_setMouseDeltaMode(true);
	Shell_redisplay();
	return true;
}

static bool mouseUp(Atom eventID, void * eventData, void * context) {
	Shell_setMouseDeltaMode(false);
	Shell_redisplay();
	return true;
}

#define CAMERA_LOOK_SENSITIVITY 0x00100
#define CAMERA_ZOOM_SENSITIVITY 0x00060
#define CAMERA_DRAG_SENSITIVITY 0x00060

static bool mouseDragged(Atom eventID, void * eventData, void * context) {
	TrimeshViewerScreen * self = context;
	struct mouseEvent * event = eventData;
	Vector3x offset;
	
	if (event->modifiers & MODIFIER_SHIFT_BIT) {
		// Pan
		if (event->modifiers & MODIFIER_CONTROL_BIT) {
			offset.x = 0x00000;
			offset.y = 0x00000;
			offset.z = -xmul(ftox(event->delta.y), xmul(CAMERA_DRAG_SENSITIVITY, self->cameraDistance));
		} else {
			offset.x = xmul(ftox(event->delta.x), xmul(CAMERA_DRAG_SENSITIVITY, self->cameraDistance));
			offset.y = -xmul(ftox(event->delta.y), xmul(CAMERA_DRAG_SENSITIVITY, self->cameraDistance));
			offset.z = 0x00000;
		}
		offset = Quaternionx_multiplyVector3x(Quaternionx_inverted(self->cameraDirection), offset);
		self->cameraFocus = Vector3x_add(self->cameraFocus, offset);
		Shell_redisplay();
		
	} else if (event->modifiers & MODIFIER_CONTROL_BIT) {
		// Zoom
		self->cameraDistance += xmul(ftox(event->delta.y), xmul(CAMERA_ZOOM_SENSITIVITY, self->cameraDistance));
		if (self->cameraDistance < 0x10000) {
			self->cameraDistance = 0x10000;
		}
		Shell_redisplay();
		
	} else {
		// Rotate
		Quaternionx_rotate(&self->cameraDirection, VECTOR3x_UP, xmul(ftox(event->delta.x), CAMERA_LOOK_SENSITIVITY));
		Quaternionx_rotate(&self->cameraDirection, Quaternionx_multiplyVector3x(Quaternionx_inverted(self->cameraDirection), VECTOR3x_RIGHT), xmul(ftox(event->delta.y), CAMERA_LOOK_SENSITIVITY));
		Shell_redisplay();
	}
	
	return true;
}

static bool resized(Atom eventID, void * eventData, void * context) {
	TrimeshViewerScreen * self = context;
	Shell_redisplay();
	call_virtual(setProjectionMatrix, self->shaderConfiguration, Matrix4x4f_perspective(MATRIX4x4f_IDENTITY, 70.0f, g_viewRatio, 0.5f, 800.0f));
	return true;
}

void TrimeshViewerScreen_activate(TrimeshViewerScreen * self, Screen * lastScreen, const char * transitionName) {
	loadTrimeshPreset1(self);
	self->trimesh = NULL;
	self->cameraFocus = VECTOR3x_ZERO;
	self->cameraDirection = QUATERNIONx_IDENTITY;
	self->cameraDistance = 0xF0000;
	
	EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_KEY_DOWN), keyDown, self);
	EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_MOUSE_DOWN), mouseDown, self);
	EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_MOUSE_UP), mouseUp, self);
	EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_MOUSE_DRAGGED), mouseDragged, self);
	EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_RESIZED), resized, self);
	EventDispatcher_registerForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_DRAW), draw, self);
	
	call_virtual(setProjectionMatrix, self->shaderConfiguration, Matrix4x4f_perspective(MATRIX4x4f_IDENTITY, 70.0f, g_viewRatio, 0.5f, 800.0f));
	call_virtual(setLights, self->shaderConfiguration, VECTOR3f(0.0f, 8.0f, 8.0f), COLOR3f(1.0f, 1.0f, 1.0f), VECTOR3f(-1.0f, -2.0f, -8.0f), COLOR3f(0.8f, 0.8f, 0.8f), VECTOR3f(0.0f, -1.0f, 0.0f), COLOR3f(0.1f, 0.1f, 0.1f), COLOR3f(0.2f, 0.2f, 0.105f));
	
	Shell_redisplay();
}

void TrimeshViewerScreen_deactivate(TrimeshViewerScreen * self, Screen * nextScreen, const char * transitionName) {
	if (self->trimesh != NULL) {
		CollisionStaticTrimesh_dispose(self->trimesh);
		self->trimesh = NULL;
	}
	
	EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_KEY_DOWN), keyDown, self);
	EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_MOUSE_DOWN), mouseDown, self);
	EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_MOUSE_UP), mouseUp, self);
	EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_MOUSE_DRAGGED), mouseDragged, self);
	EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_RESIZED), resized, self);
	EventDispatcher_unregisterForEvent(self->screenManager->eventDispatcher, ATOM(EVENT_DRAW), draw, self);
}
