#include "shell/Shell.h"
#include "shell/ShellCallbacks.h"
#include "shell/ShellKeyCodes.h"
#include "3dmodelio/TextureAtlasData.h"
#include "gamemath/Matrix4x4f.h"
#include "gamemath/MouseCoordinateTransforms.h"
#include "font/BitmapFont.h"
#include "font/TextFlow.h"
#include "jsonserialization/JSONDeserializationContext.h"
#include "renderer/Drawing.h"
#include "renderer/Renderable.h"
#include "renderer/Renderer.h"
#include "shadercollection/ShaderCollection.h"
#include "shadercollection/ShaderConfiguration2DTexture.h"
#include "imageio/ImageIO.h"
#include "utilities/AutoFreePool.h"
#include "utilities/IOUtilities.h"

#if defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos)
#include "eaglshell/EAGLShell.h"
#include "eaglshell/EAGLTarget.h"
#elif defined(STEM_PLATFORM_macosx)
#include "nsopenglshell/NSOpenGLShell.h"
#include "nsopenglshell/NSOpenGLTarget.h"
#elif defined(STEM_PLATFORM_windows)
#include "wglshell/WGLShell.h"
#include "wglshell/WGLTarget.h"
#elif defined(STEM_PLATFORM_linux)
#include "glxshell/GLXShell.h"
#include "glxshell/GLXTarget.h"
#elif defined(STEM_PLATFORM_android)
#include "eglshell/EGLShell.h"
#include "eglshell/EGLTarget.h"
#else
#error Unsupported platform
#endif

#include <stddef.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#define FREEFORM_LENGTH_MAX 100

static char freeformText[FREEFORM_LENGTH_MAX + 1];
static unsigned int viewportWidth = 800, viewportHeight = 600;
static float viewportRatio = (800.0f / 600.0f);
static BitmapFont * font;
static TextFlow * textFlow;
static const char * jsonPath = NULL;
static size_t lastIndexAtPositionX = 0;
static bool lastLeadingEdge = false;
static Vector2f lastPositionAtIndex = {0.0f, -1.0f};
static float scale = 1.0f;
static bool draggingTextFlow, draggingClipBounds1, draggingClipBounds2;
static float lastDragX, lastDragY;
static float textFlowWidth;
static Rect4f clipBounds;
static bool clipping;
static Renderer * renderer;
static RenderPipelineConfiguration renderPipelineConfiguration;
static RenderLayer * renderLayer;
static ShaderConfiguration2DTexture * shaderConfigurationText;
static ShaderConfiguration * shaderConfigurationLine;
static Renderable * textSprite, * lineSprite;
static Texture * fontTexture;

struct drawStringContext {
	Color4f color;
	VertexIO * vertexIO;
};

static void writeVerticesGlyphCallback(BitmapFont * font, Rect4f vertexBounds, Rect4f textureBounds, void * context) {
	struct drawStringContext * contextStruct = context;
	struct vertex_p2f_t2f_c4f vertices[4], vertex = {{vertexBounds.xMin, vertexBounds.yMin}, {textureBounds.xMin, textureBounds.yMin}, {contextStruct->color.red, contextStruct->color.green, contextStruct->color.blue, contextStruct->color.alpha}};
	vertices[0] = vertex;
	vertex.position[0] = vertexBounds.xMax;
	vertex.texCoords[0] = textureBounds.xMax;
	vertices[1] = vertex;
	vertex.position[1] = vertexBounds.yMax;
	vertex.texCoords[1] = textureBounds.yMax;
	vertices[2] = vertex;
	vertex.position[0] = vertexBounds.xMin;
	vertex.texCoords[0] = textureBounds.xMin;
	vertices[3] = vertex;
	uint32_t indexes[6] = {0, 1, 2, 2, 3, 0};
	VertexIO_writeIndexedVertices(contextStruct->vertexIO, 4, vertices, 6, indexes);
}

static void drawString(const char * string, size_t length, Color4f color, float emSize, float offsetX, float offsetY, VertexIO * vertexIO) {
	struct drawStringContext contextStruct = {color, vertexIO};
	BitmapFont_writeStringWithCallback(font, string, length, emSize, VECTOR2f(offsetX, offsetY), VECTOR2f_ZERO, 0, writeVerticesGlyphCallback, &contextStruct);
}

static void writeTextVertices(Renderable * self, VertexIO * vertexIO, void * context) {
	struct drawStringContext contextStruct = {COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), vertexIO};
	unsigned int lastIndex = vertexIO->indexCount;
	TextFlow_writeStringWithCallback(textFlow, 0.0625f * scale, VECTOR2f(-6.0f * 0.0625f * scale, 0.75f), VECTOR2f(0.0f, 1.0f), 0, writeVerticesGlyphCallback, &contextStruct);
	if (clipping) {
		clipVerticesInsideRect(lastIndex, vertexIO->indexCount - lastIndex, clipBounds, vertexIO);
	}
	
	float stringWidth = BitmapFont_measureString(font, "Hello, world!", 13, 0);
	drawString("Hello, world!", 13, COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), 0.1f * scale, stringWidth * -0.05f * scale, 0.0f, vertexIO);
	
	stringWidth = BitmapFont_measureString(font, freeformText, strlen(freeformText), 0);
	drawString(freeformText, strlen(freeformText), COLOR4f(1.0f, 1.0f, 1.0f, 1.0f), 0.0625f * scale, stringWidth * -0.03125f * scale, -0.0625f * scale, vertexIO);
	
	char indexString[32];
	snprintf(indexString, 32, "%u, %s", (unsigned int) lastIndexAtPositionX, lastLeadingEdge ? "true" : "false");
	stringWidth = BitmapFont_measureString(font, indexString, strlen(indexString), 0);
	drawString(indexString, strlen(indexString), COLOR4f(0.875f, 0.875f, 0.5f, 1.0f), 0.05f * scale, stringWidth * -0.025f * scale, 0.1f * scale, vertexIO);
}

static void writeLineVertices(Renderable * self, VertexIO * vertexIO, void * context) {
	struct vertex_p2f_c4f vertices[4];
	uint32_t indexes[8];
	if (clipping) {
		struct vertex_p2f_c4f vertex = {{clipBounds.xMin, clipBounds.yMin}, {0.5f, 0.0f, 0.0f, 1.0f}};
		vertices[0] = vertex;
		vertex.position[0] = clipBounds.xMax;
		vertices[1] = vertex;
		vertex.position[1] = clipBounds.yMax;
		vertices[2] = vertex;
		vertex.position[0] = clipBounds.xMin;
		vertices[3] = vertex;
		
		indexes[0] = 0;
		indexes[1] = 1;
		indexes[2] = 1;
		indexes[3] = 2;
		indexes[4] = 2;
		indexes[5] = 3;
		indexes[6] = 3;
		indexes[7] = 0;
		
		VertexIO_writeIndexedVertices(vertexIO, 4, vertices, 8, indexes);
	}
	
	struct vertex_p2f_c4f vertex = {{(-6.0f + textFlowWidth) * 0.0625f * scale, -1.0f}, {0.5f, 0.5f, 0.0f, 1.0f}};
	vertices[0] = vertex;
	vertex.position[1] = 1.0f;
	vertices[1] = vertex;
	indexes[0] = 0;
	indexes[1] = 1;
	VertexIO_writeIndexedVertices(vertexIO, 2, vertices, 2, indexes);
	
	if (lastPositionAtIndex.y >= 0.0f) {
		Vector2f screenPosition = VECTOR2f((-6.0f + lastPositionAtIndex.x) * 0.0625f * scale, 0.75f - (lastPositionAtIndex.y + 0.5f) * 0.0625f * scale);
		struct vertex_p2f_c4f vertex = {{0.0f, 0.0f}, {0.0f, 1.0f, 0.0f, 1.0f}};
		vertex.position[0] = screenPosition.x - 0.015625f;
		vertex.position[1] = screenPosition.y;
		vertices[0] = vertex;
		vertex.position[0] = screenPosition.x;
		vertex.position[1] = screenPosition.y - 0.015625f;
		vertices[1] = vertex;
		vertex.position[0] = screenPosition.x + 0.015625f;
		vertex.position[1] = screenPosition.y;
		vertices[2] = vertex;
		vertex.position[0] = screenPosition.x;
		vertex.position[1] = screenPosition.y + 0.015625f;
		vertices[3] = vertex;
		
		indexes[0] = 0;
		indexes[1] = 1;
		indexes[2] = 1;
		indexes[3] = 2;
		indexes[4] = 2;
		indexes[5] = 3;
		indexes[6] = 3;
		indexes[7] = 0;
		
		VertexIO_writeIndexedVertices(vertexIO, 4, vertices, 8, indexes);
	}
}

static bool Target_draw(double referenceTime, double activeDrawDelta) {
	Renderer_clear(renderer, COLOR4f(0.0f, 0.0f, 0.0f, 0.0f));
	Renderer_drawLayer(renderer, renderLayer, referenceTime, activeDrawDelta);
	AutoFreePool_empty();
	return true;
}

static void Target_resized(unsigned int newWidth, unsigned int newHeight, double referenceTime) {
	viewportWidth = newWidth;
	viewportHeight = newHeight;
	viewportRatio = (float) viewportWidth / viewportHeight;
	if (renderer != NULL) {
		Renderer_setViewport(renderer, 0, 0, newWidth, newHeight, NULL);
		call_virtual(setProjectionMatrix, shaderConfigurationText, Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, -viewportRatio, viewportRatio, -1.0f, 1.0f, -1.0f, 1.0f));
	}
}

static void Target_keyDown(unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, double referenceTime) {
	if (keyCode == KEY_CODE_Q && (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT)) {
		exit(EXIT_SUCCESS);
		
	} else if (keyCode == KEY_CODE_BACKSPACE && strlen(freeformText) > 0) {
		freeformText[strlen(freeformText) - 1] = '\x00';
		Shell_redisplay();
		
	} else if (keyCode == KEY_CODE_TAB) {
		if (modifiers & MODIFIER_SHIFT_BIT) {
			clipping = !clipping;
		} else {
			textFlow->wrapMode = (textFlow->wrapMode == WORD_WRAP_NORMAL ? WORD_WRAP_AGGRESSIVE : WORD_WRAP_NORMAL);
		}
		Shell_redisplay();
		
	} else if (charCode >= BITMAPFONT_PRINTABLE_MIN && charCode <= BITMAPFONT_PRINTABLE_MAX && strlen(freeformText) < FREEFORM_LENGTH_MAX) {
		freeformText[strlen(freeformText)] = charCode;
		Shell_redisplay();
		
#if defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos)
	} else if (keyCode == KEYBOARD_RETURN_OR_ENTER) {
		EAGLShell_hideKeyboard();
#endif
	}
}

static void Target_keyModifiersChanged(unsigned int modifiers, unsigned int lastModifiers, double referenceTime) {
}

static void queryIndexAtPosition(float x, float y) {
	if (y < 0.0f) {
		lastIndexAtPositionX = BitmapFont_indexAtPositionX(font, freeformText, TEXT_STRLEN, 0.0625f, x, 0.5f, TEXT_OPTION_NO_TERMINATING_ADVANCE, &lastLeadingEdge);
		lastPositionAtIndex = VECTOR2f(0.0f, -1.0f);
		
	} else if (y < 0.1f) {
		lastIndexAtPositionX = BitmapFont_indexAtPositionX(font, "Hello, world!", 13, 0.1f, x, 0.5f, TEXT_OPTION_NO_TERMINATING_ADVANCE, &lastLeadingEdge);
		lastPositionAtIndex = VECTOR2f(0.0f, -1.0f);
		
	} else {
		lastIndexAtPositionX = TextFlow_indexAtPosition(textFlow, VECTOR2f((x + 6.0f * 0.0625f) / 0.0625f, -(y - 0.75f) / 0.0625f), TEXT_OPTION_NO_TERMINATING_ADVANCE, &lastLeadingEdge);
		lastPositionAtIndex = TextFlow_positionAtIndex(textFlow, lastIndexAtPositionX, TEXT_OPTION_NO_TERMINATING_ADVANCE);
	}
	
	Shell_redisplay();
}

static void Target_mouseDown(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifierFlags, double referenceTime) {
	Vector2f transformedPosition;
	
	transformedPosition = transformMousePosition_signedCenter(VECTOR2f(x, y), viewportWidth, viewportHeight, 1.0f);
	lastDragX = transformedPosition.x;
	lastDragY = transformedPosition.y;
	
	if (clipping && (modifierFlags & MODIFIER_CONTROL_BIT)) {
		if (modifierFlags & MODIFIER_SHIFT_BIT) {
			draggingClipBounds1 = true;
		} else {
			draggingClipBounds2 = true;
		}
	} else if (modifierFlags & MODIFIER_SHIFT_BIT) {
		draggingTextFlow = true;
	} else {
		queryIndexAtPosition(transformedPosition.x, transformedPosition.y);
	}
#if defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos)
	if (buttonNumber > 0) {
		EAGLShell_showKeyboard();
	}
#endif
}

static void Target_mouseDragged(unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifierFlags, double referenceTime) {
	Vector2f transformedPosition;
	
	transformedPosition = transformMousePosition_signedCenter(VECTOR2f(x, y), viewportWidth, viewportHeight, 1.0f);
	
	if (draggingTextFlow) {
		textFlowWidth += (transformedPosition.x - lastDragX) / (0.0625f * scale);
		textFlow->wrapWidth = textFlowWidth;
		Shell_redisplay();
		
	} else if (draggingClipBounds1) {
		clipBounds.xMin += transformedPosition.x - lastDragX;
		clipBounds.yMin += transformedPosition.y - lastDragY;
		Shell_redisplay();
		
	} else if (draggingClipBounds2) {
		clipBounds.xMax += transformedPosition.x - lastDragX;
		clipBounds.yMax += transformedPosition.y - lastDragY;
		Shell_redisplay();
		
	} else {
		queryIndexAtPosition(transformedPosition.x, transformedPosition.y);
	}
	
	lastDragX = transformedPosition.x;
	lastDragY = transformedPosition.y;
}

static void Target_mouseUp(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifierFlags, double referenceTime) {
	draggingTextFlow = false;
	draggingClipBounds1 = false;
	draggingClipBounds2 = false;
}

static void registerShellCallbacks() {
	Shell_drawFunc(Target_draw);
	Shell_resizeFunc(Target_resized);
	Shell_keyDownFunc(Target_keyDown);
	Shell_keyModifiersChangedFunc(Target_keyModifiersChanged);
	Shell_mouseDownFunc(Target_mouseDown);
	Shell_mouseDraggedFunc(Target_mouseDragged);
	Shell_mouseUpFunc(Target_mouseUp);
}

#ifndef STEM_PLATFORM_android
static void printUsage() {
	fprintf(stderr, "Usage: font_testharness [-json /path/to/font.json] [-scale %%f]\n");
}

static void parseArgs(int argc, const char ** argv) {
	int argIndex;
	bool printUsageAfterParsing = false;
	
	for (argIndex = 1; argIndex < argc; argIndex++) {
		if (!strcmp(argv[argIndex], "--help")) {
			printUsageAfterParsing = true;
			
		} else if (!strcmp(argv[argIndex], "-json")) {
			argIndex++;
			if (argIndex >= argc) {
				printf("-json specified as last argument; must be followed by a file path\n");
				printUsageAfterParsing = true;
				break;
			}
			jsonPath = argv[argIndex];
			
		} else if (!strcmp(argv[argIndex], "-scale")) {
			argIndex++;
			if (argIndex >= argc) {
				printf("-scale specified as last argument; must be followed by a number\n");
				printUsageAfterParsing = true;
				break;
			}
			if (!sscanf(argv[argIndex], "%f", &scale)) {
				printf("Couldn't parse value specified to -scale (%s) as a number\n", argv[argIndex]);
				printUsageAfterParsing = true;
				break;
			}
			
		} else {
			printf("Unrecognized argument: %s\n", argv[argIndex]);
			printUsageAfterParsing = true;
		}
	}
	if (printUsageAfterParsing) {
		printUsage();
	}
}
#endif

#if defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos)
void EAGLTarget_configure(int argc, char ** argv, struct EAGLShellConfiguration * configuration) {
	parseArgs(argc, (const char **) argv);
	configuration->preferredOpenGLAPIVersion = EAGLShellOpenGLVersion_ES1 | EAGLShellOpenGLVersion_ES2;
#elif defined(STEM_PLATFORM_macosx)
void NSOpenGLTarget_configure(int argc, const char ** argv, struct NSOpenGLShellConfiguration * configuration) {
	parseArgs(argc, argv);
	configuration->windowTitle = "Font Test Harness";
	configuration->useGLCoreProfile = true;
#elif defined(STEM_PLATFORM_windows)
void WGLTarget_configure(void * instance, void * prevInstance, char * commandLine, int command, int argc, const char ** argv, struct WGLShellConfiguration * configuration) {
	parseArgs(argc, argv);
	configuration->windowTitle = "Font Test Harness";
	configuration->useGLCoreProfile = true;
#elif defined(STEM_PLATFORM_linux)
void GLXTarget_configure(int argc, const char ** argv, struct GLXShellConfiguration * configuration) {
	parseArgs(argc, argv);
#include "IconData_stemapp.h"
	configuration->icon.data = STATIC_iconData_stemapp;
	configuration->icon.size = sizeof(STATIC_iconData_stemapp) / sizeof(STATIC_iconData_stemapp[0]);
	configuration->applicationName = STEM_HUMAN_READABLE_TARGET_NAME;
	configuration->windowTitle = "Font Test Harness";
#elif defined(STEM_PLATFORM_android)
void EGLTarget_configure(struct EGLShellConfiguration * configuration) {
	readResourceFile = EGLShell_readResourceFile;
#else
#error Unsupported platform
#endif
	registerShellCallbacks();
}

void Target_init() {
	renderer = Renderer_create();
	renderPipelineConfiguration = default2DRenderPipelineConfiguration(RENDER_BLEND_ALPHA);
	renderLayer = RenderLayer_create(RENDER_LAYER_SORT_NONE, NULL, NULL);
	
	JSONDeserializationContext * context;
	const char * fontJSONFilePath;
	if (jsonPath == NULL) {
		chdir(Shell_getResourcePath());
		fontJSONFilePath = "test_font.json";
		context = JSONDeserializationContext_createWithResourceFile(fontJSONFilePath);
	} else {
		fontJSONFilePath = jsonPath;
		context = JSONDeserializationContext_createWithFile(fontJSONFilePath);
	}
	
	if (context->status != SERIALIZATION_ERROR_OK) {
		fprintf(stderr, "Fatal error: Couldn't load %s (status %d)\n", fontJSONFilePath, context->status);
		exit(EXIT_FAILURE);
	}
	font = BitmapFont_deserialize(context);
	call_virtual(dispose, context);
	if (font == NULL) {
		fprintf(stderr, "Fatal error: Couldn't deserialize %s (status %d)\n", fontJSONFilePath, context->status);
		exit(EXIT_FAILURE);
	}
	
	char atlasJSONFilePath[PATH_MAX];
	if (jsonPath == NULL) {
		strncpy_safe(atlasJSONFilePath, font->atlasName, sizeof(atlasJSONFilePath));
		context = JSONDeserializationContext_createWithResourceFile(atlasJSONFilePath);
		
	} else {
		size_t charIndex;
		
		strncpy_safe(atlasJSONFilePath, jsonPath, sizeof(atlasJSONFilePath));
		for (charIndex = strlen(atlasJSONFilePath) - 1; charIndex > 0; charIndex--) {
			if (atlasJSONFilePath[charIndex] == '/') {
				charIndex++;
				break;
			}
		}
		strncpy_safe(atlasJSONFilePath + charIndex, font->atlasName, sizeof(atlasJSONFilePath) - charIndex);
		context = JSONDeserializationContext_createWithFile(atlasJSONFilePath);
	}
	
	if (context->status != SERIALIZATION_ERROR_OK) {
		fprintf(stderr, "Fatal error: Couldn't load %s (status %d)\n", atlasJSONFilePath, context->status);
		exit(EXIT_FAILURE);
	}
	TextureAtlasData * atlasData = TextureAtlasData_deserialize(context);
	call_virtual(dispose, context);
	if (atlasData == NULL) {
		fprintf(stderr, "Fatal error: Couldn't deserialize %s (status %d)\n", atlasJSONFilePath, context->status);
		exit(EXIT_FAILURE);
	}
	
	BitmapImage * image;
	char textureImageFilePath[PATH_MAX];
	if (jsonPath == NULL) {
		strncpy_safe(textureImageFilePath, atlasData->textureName, sizeof(textureImageFilePath));
		image = ImageIO_readImageResourceFile(textureImageFilePath, BITMAP_PIXEL_FORMAT_RGBA_8888, true);
		
	} else {
		size_t charIndex;
		
		strncpy_safe(textureImageFilePath, jsonPath, sizeof(textureImageFilePath));
		for (charIndex = strlen(textureImageFilePath) - 1; charIndex > 0; charIndex--) {
			if (textureImageFilePath[charIndex] == '/') {
				charIndex++;
				break;
			}
		}
		strncpy_safe(textureImageFilePath + charIndex, atlasData->textureName, sizeof(textureImageFilePath) - charIndex);
		image = ImageIO_readImageFile(textureImageFilePath, BITMAP_PIXEL_FORMAT_RGBA_8888, true);
	}
	
	if (image == NULL) {
		fprintf(stderr, "Fatal error: Couldn't load %s\n", textureImageFilePath);
		exit(EXIT_FAILURE);
	}
	TextureAtlas * atlas = TextureAtlasData_createTextureAtlas(atlasData, image->width, image->height);
	fontTexture = Texture_createWithTexels2D(image->pixels, image->width, image->height, 0, 4, TEXEL_COMPONENT_UINT8_NORM, TEXEL_SWIZZLE_DEFAULT, 0);
	call_virtual(dispose, image);
	BitmapFont_readAtlasEntries(font, atlas);
	TextureAtlasData_dispose(atlasData);
	
	textFlowWidth = 12.0f;
	clipBounds = RECT4f(-6.0f * 0.0625f, 6.0f * 0.0625f, 0.25f, 0.75f);
	textFlow = TextFlow_create(font, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut eleifend commodo placerat. Aenean a viverra leo.", false, false, ALIGN_LEFT, WORD_WRAP_NORMAL, textFlowWidth);
	
	shaderConfigurationText = ShaderConfiguration2DTexture_create(ShaderCollection_get2DTextureShader());
	call_virtual(setTexture, shaderConfigurationText, 0, fontTexture, false);
	call_virtual(setProjectionMatrix, shaderConfigurationText, Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, -viewportRatio, viewportRatio, -1.0f, 1.0f, -1.0f, 1.0f));
	shaderConfigurationLine = ShaderConfiguration_create(ShaderCollection_get2DColorShader());
	call_virtual(referenceUniformFromConfiguration, shaderConfigurationLine, ATOM("projectionTransform"), shaderConfigurationText, ATOM("projectionTransform"));
	textSprite = Renderable_createWithCallback(PRIMITIVE_TRIANGLES, &renderPipelineConfiguration, shaderConfigurationText, writeTextVertices, NULL, NULL);
	lineSprite = Renderable_createWithCallback(PRIMITIVE_LINES, &renderPipelineConfiguration, shaderConfigurationLine, writeLineVertices, NULL, NULL);
	RenderLayer_addRenderable(renderLayer, textSprite, 0, RECT4i_EMPTY);
	RenderLayer_addRenderable(renderLayer, lineSprite, 0, RECT4i_EMPTY);
	
	memset(freeformText, 0, FREEFORM_LENGTH_MAX + 1);
	Shell_mainLoop();
}
