#include "gamemath/Matrix4x4f.h"
#include "gamemath/Scalar.h"
#include "renderer/Drawing.h"
#include "renderer/GLIncludes.h"
#include "renderer/GraphicsDriver.h"
#include "renderer/Renderable.h"
#include "renderer/Renderer.h"
#include "shell/Shell.h"
#include "shell/ShellCallbacks.h"
#include "shell/ShellKeyCodes.h"

#if defined(STEM_PLATFORM_macosx)
#include "nsopenglshell/NSOpenGLShell.h"
#include "nsopenglshell/NSOpenGLTarget.h"
#elif defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos)
#include "eaglshell/EAGLShell.h"
#include "eaglshell/EAGLTarget.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 "stem_core.h"
#include <float.h>
#include <math.h>
#include <stdio.h>
#include <unistd.h>

#ifdef STEM_PLATFORM_android
#include <android/log.h>
#define printf(format, ...) __android_log_print(ANDROID_LOG_INFO, "Renderer", format, ##__VA_ARGS__);
#define fprintf(stderr, format, ...) __android_log_print(ANDROID_LOG_INFO, "Renderer", format, ##__VA_ARGS__);
#endif

#define PROJECTION_FOV 60.0f
#define FBO_WIDTH 200
#define FBO_HEIGHT 150

static Renderer * renderer;
static RenderLayer * renderLayer2D;
static RenderLayer * renderLayer3D;
//static Texture * checkerboardTexture;
//static Texture * solidColorTexture;
//static VertexBuffer * vertexBuffer;
static ShaderUniformConfiguration perspectiveProjectionUniform;
static ShaderUniformConfiguration orthoProjectionUniform;
//static ShaderUniformConfiguration viewTransformUniform;
static ShaderConfiguration * renderTargetShaderConfiguration;
static unsigned int viewWidth = 1280, viewHeight = 720;
static float viewRatio = 16.0f / 9.0f;
static float scaleFactor = 1.0f;
//static double animationStartTime, lastAnimationTime;
static bool spriteMode;
static RenderTarget * renderTarget;
static bool useFBO;
static Color4f clearColor;

static bool Target_draw(double referenceTime, double activeDrawDelta) {
	if (useFBO) {
		Renderer_setRenderTarget(renderer, renderTarget);
	}
	
	Renderer_clear(renderer, clearColor);
	if (spriteMode) {
		Renderer_drawLayer(renderer, renderLayer2D, referenceTime, activeDrawDelta);
	} else {
		//ShaderUniformConfiguration_setMat4_Matrix4x4f(&viewTransformUniform, call_virtual(getTransform, camera));
		Renderer_drawLayer(renderer, renderLayer3D, referenceTime, activeDrawDelta);
	}
	
	if (useFBO) {
		Renderer_setRenderTarget(renderer, NULL);
		Renderer_clear(renderer, clearColor);
		drawRenderTarget(renderer, renderTarget, renderTargetShaderConfiguration);
	}
	
	return true;
}

static void updateProjection() {
	float ratio;
	
	if (useFBO) {
		ratio = (float) FBO_WIDTH / FBO_HEIGHT;
	} else {
		ratio = viewRatio;
	}
	
	Matrix4x4f perspectiveMatrix = Matrix4x4f_perspective(MATRIX4x4f_IDENTITY, PROJECTION_FOV, ratio, 0.5f, 100.0f);
	ShaderUniformConfiguration_setMat4_Matrix4x4f(&perspectiveProjectionUniform, perspectiveMatrix);
	
	Matrix4x4f orthoMatrix = Matrix4x4f_ortho(MATRIX4x4f_IDENTITY, -ratio, ratio, -1.0f, 1.0f, -1.0f, 1.0f);
	ShaderUniformConfiguration_setMat4_Matrix4x4f(&orthoProjectionUniform, orthoMatrix);
}

static void writeRenderTargetVertices(Renderable * renderable, VertexIO * vertexIO, void * context) {
	float vertices[8] = {
		0.0f, 0.0f,
		1.0f, 0.0f,
		1.0f, 1.0f,
		0.0f, 1.0f
	};
	uint32_t indexes[6] = {0, 1, 2, 2, 3, 0};
	VertexIO_writeIndexedVertices(vertexIO, 4, vertices, 6, indexes);
}

static void testRenderTargets(void) {
	RenderTarget * testRenderTarget = RenderTarget_createEmpty(1, 1, 0);
	RenderTarget_attachRenderbuffer(testRenderTarget, RenderTarget_attachment_color0, RenderTarget_renderbufferFormat_rgba8);
	RenderTarget_finalizeAttachments(testRenderTarget);
	Renderer_setRenderTarget(renderer, testRenderTarget);
	Renderer_clear(renderer, C4f(0.25f, 0.5f, 0.75f, 1.0f));
	Renderer_setRenderTarget(renderer, NULL);
	uint8_t colorValues[4];
	RenderTarget_readData(testRenderTarget, colorValues, 0, RenderTarget_pixelFormat_rgba, RenderTarget_dataType_uint8, 0, 0, 1, 1);
	printf("Read RGBA data: %02X %02X %02X %02X\n", colorValues[0], colorValues[1], colorValues[2], colorValues[3]);
	RenderTarget_dispose(testRenderTarget);
	
	testRenderTarget = RenderTarget_createEmpty(1, 1, 0);
	RenderTarget_attachRenderbuffer(testRenderTarget, RenderTarget_attachment_color0, RenderTarget_renderbufferFormat_r32ui);
	RenderTarget_attachRenderbuffer(testRenderTarget, RenderTarget_attachment_color1, RenderTarget_renderbufferFormat_rg32f);
	RenderTarget_finalizeAttachments(testRenderTarget);
	Renderer_clear(renderer, C4f(0.25f, 0.5f, 0.75f, 1.0f));
	const char vertexShader[] = 
"#version 150\n"
"in vec2 position;\n"
"void main() {\n"
"	gl_Position = vec4(position, 0.0, 1.0);\n"
"}";
	const char fragmentShader[] = 
"#version 150\n"
"out uint fragIndex;\n"
"out vec2 fragTexCoord;\n"
"void main() {\n"
"	fragIndex = 5u;\n"
"	fragTexCoord = vec2(2.0, 3.0);\n"
"}";
	VertexAttributeTypeSpec vertexAttributes[] = {
		{"position", ATTRIBUTE_TYPE_FLOAT, ATTRIBUTE_USAGE_POSITION, 2}
	};
	Shader * testShader = Shader_createUnlinked(vertexShader, sizeof(vertexShader) - 1, fragmentShader, sizeof(fragmentShader) - 1, 0, VertexFormat_create(sizeof_count(vertexAttributes), vertexAttributes), true);
	Shader_specifyOutput(testShader, 0, "fragIndex");
	Shader_specifyOutput(testShader, 1, "fragTexCoord");
	Shader_link(testShader);
	ShaderConfiguration * shaderConfiguration = ShaderConfiguration_create(testShader);
	RenderPipelineConfiguration pipelineConfiguration = default2DRenderPipelineConfiguration(RENDER_BLEND_NONE);
	Renderer_setRenderTarget(renderer, testRenderTarget);
	Renderer_drawWithCallback(renderer, &pipelineConfiguration, PRIMITIVE_TRIANGLES, shaderConfiguration, RECT4i_EMPTY, 0.0, 0.0, writeRenderTargetVertices, NULL);
	Renderer_setRenderTarget(renderer, NULL);
	uint32_t uint32Value;
	float floatValues[2];
	RenderTarget_readData(testRenderTarget, &uint32Value, 0, RenderTarget_pixelFormat_redi, RenderTarget_dataType_uint32, 0, 0, 1, 1);
	RenderTarget_readData(testRenderTarget, floatValues, 1, RenderTarget_pixelFormat_rg, RenderTarget_dataType_float32, 0, 0, 1, 1);
	printf("Read uint32 data: 0x%08X\n", uint32Value);
	printf("Read float data: %f, %f\n", floatValues[0], floatValues[1]);
	ShaderConfiguration_dispose(shaderConfiguration);
	Shader_dispose(testShader);
	RenderTarget_dispose(testRenderTarget);
}

static void Target_keyDown(unsigned int charCode, unsigned int keyCode, unsigned int modifiers, bool isRepeat, double referenceTime) {
	switch (keyCode) {
		case KEY_CODE_Q:
			if (modifiers & MODIFIER_PLATFORM_MENU_COMMAND_BIT) {
				exit(EXIT_SUCCESS);
			}
			break;
			
		case KEY_CODE_E:
			if (!isRepeat) {
				GLint extensionCount = 0;
				glGetIntegerv(GL_NUM_EXTENSIONS, &extensionCount);
				for (GLint extensionIndex = 0; extensionIndex < extensionCount; extensionIndex++) {
					const GLubyte * extensionString = glGetStringi(GL_EXTENSIONS, extensionIndex);
					printf("%s\n", extensionString);
				}
			}
			break;
			
		case KEY_CODE_C:
			printf("GraphicsDriver_textureDimensionMax: %d\n", GraphicsDriver_getCapability(GraphicsDriver_textureDimensionMax));
			printf("GraphicsDriver_renderBufferDimensionMax: %d\n", GraphicsDriver_getCapability(GraphicsDriver_renderBufferDimensionMax));
			printf("GraphicsDriver_vertexAttribCountMax: %d\n", GraphicsDriver_getCapability(GraphicsDriver_vertexAttribCountMax));
			printf("GraphicsDriver_textureImageUnitCount: %d\n", GraphicsDriver_getCapability(GraphicsDriver_textureImageUnitCount));
			printf("GraphicsDriver_sampleCountMax: %d\n", GraphicsDriver_getCapability(GraphicsDriver_sampleCountMax));
			printf("GraphicsDriver_colorAttachmentCountMax: %d\n", GraphicsDriver_getCapability(GraphicsDriver_colorAttachmentCountMax));
			break;
			
		case KEY_CODE_T:
			testRenderTargets();
			break;
	}
}

static void Target_keyUp(unsigned int keyCode, unsigned int modifiers, double referenceTime) {
}

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

static void Target_mouseDown(unsigned int buttonNumber, unsigned int buttonMask, float x, float y, unsigned int modifierFlags, double referenceTime) {
	Shell_setMouseDeltaMode(true);
}

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

static void Target_mouseMoved(float x, float y, float deltaX, float deltaY, unsigned int modifierFlags, double referenceTime) {
}

#define SPRITE_DRAG_RATIO 0.003125f
#define THRESHOLD_DRAG_RATIO 0.000625f
#define SMOOTH_RANGE_DRAG_RATIO 0.0000625f

static void Target_mouseDragged(unsigned int buttonMask, float x, float y, float deltaX, float deltaY, unsigned int modifierFlags, double referenceTime) {
	deltaX /= scaleFactor;
	deltaY /= scaleFactor;
}

static void Target_resized(unsigned int newWidth, unsigned int newHeight, double referenceTime) {
	viewWidth = newWidth;
	viewHeight = newHeight;
	viewRatio = (float) newWidth / newHeight;
	
	if (renderer != NULL) {
		Renderer_setViewport(renderer, 0, 0, viewWidth, viewHeight, NULL);
		updateProjection();
	}
}

static void Target_backgrounded(double referenceTime) {
}

static void Target_foregrounded(double referenceTime) {
}

static void registerShellCallbacks() {
	Shell_drawFunc(Target_draw);
	Shell_resizeFunc(Target_resized);
	Shell_keyDownFunc(Target_keyDown);
	Shell_keyUpFunc(Target_keyUp);
	Shell_keyModifiersChangedFunc(Target_keyModifiersChanged);
	Shell_mouseDownFunc(Target_mouseDown);
	Shell_mouseUpFunc(Target_mouseUp);
	Shell_mouseMovedFunc(Target_mouseMoved);
	Shell_mouseDraggedFunc(Target_mouseDragged);
	Shell_backgroundedFunc(Target_backgrounded);
	Shell_foregroundedFunc(Target_foregrounded);
}

#if defined(STEM_PLATFORM_macosx)
void NSOpenGLTarget_configure(int argc, const char ** argv, struct NSOpenGLShellConfiguration * configuration) {
	configuration->windowTitle = "Renderer";
	configuration->useGLCoreProfile = true;
	configuration->displayMode.depthBuffer = true;
#elif defined(STEM_PLATFORM_iphonesimulator) || defined(STEM_PLATFORM_iphoneos)
void EAGLTarget_configure(int argc, char ** argv, struct EAGLShellConfiguration * configuration) {
	configuration->displayMode.depthAttachment = true;
	configuration->displayMode.depthPrecision = 24;
#elif defined(STEM_PLATFORM_windows)
void WGLTarget_configure(void * instance, void * prevInstance, char * commandLine, int command, int argc, const char ** argv, struct WGLShellConfiguration * configuration) {
	configuration->windowTitle = "Renderer";
	configuration->useGLCoreProfile = true;
	configuration->displayMode.depthBuffer = true;
#ifdef STEM_ARCH_x86_64
	WGLShell_redirectStdoutToFile("stdout.txt");
#endif
#elif defined(STEM_PLATFORM_linux)
void GLXTarget_configure(int argc, const char ** argv, struct GLXShellConfiguration * configuration) {
#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 = "Renderer";
	configuration->displayMode.depthBuffer = true;
#elif defined(STEM_PLATFORM_android)
void EGLTarget_configure(struct EGLShellConfiguration * configuration) {
	configuration->displayMode.depthSize = 24;
#else
void GLUTTarget_configure(int argc, const char ** argv, struct GLUTShellConfiguration * configuration) {
	configuration->windowTitle = "Renderer";
	configuration->displayMode.depthBuffer = true;
#endif
	scaleFactor = Shell_getDisplayScaleFactor(Shell_getDisplayIndexFromWindow());
	registerShellCallbacks();
}

void Target_init() {
	renderer = Renderer_create();
	renderLayer2D = RenderLayer_create(RENDER_LAYER_SORT_NONE, NULL, NULL);
	renderLayer3D = RenderLayer_create(RENDER_LAYER_SORT_NONE, NULL, NULL);
	renderTarget = RenderTarget_createSimple(FBO_WIDTH, FBO_HEIGHT, true, false);
	
	// Test harness tasks:
	// - Basic renderer usage: Clear, clear depth, draw layer, draw single
	// - RenderLayer: Callback, list management, options
	// - Multiple shaders, vertex format
	// - VertexBuffer buffer/map data
	// - Texture: All types, swizzle, update options
	// - RenderTarget: Draw, readback
	// - RenderDataBuffer: Shader data transfer
	// - Renderable: All types
	// Remove SpriteRenderable? Rehome Animation/AnimationState/Armature? Camera/OrbitCamera/WalkCamera? TextureAtlas?
	// (above is all out of date; review and revise)
	
	// TODO: RenderTarget test; multiple read/write data types, multiple color buffer outputs
	
	Shell_mainLoop();
}
