#include <limits>

#include "RuntimeModule.h"
#include "Timer.h"
#include "Globals.h"
#include "RenderModule.h"
#include <GLFW/glfw3.h>
#include "Serializer.h"

#include "imgui/imgui.h"

using namespace SimpleMath::GL;

struct Renderer;

float moving_factor = 1.0f;
const int cNumSplits = 4;

struct RendererSettings {
	bool DrawLightDepth = false;
	int RenderMode = 0;
	int ActiveShadowMapSplit = 0;
	int ActiveCameraDebugUi = 0;
};

enum SceneRenderMode {
	SceneRenderModeDefault = 0,
	SceneRenderModeColor = 1,
	SceneRenderModeDepth = 2,
	SceneRenderModeNormals = 3,
	SceneRenderModePositions = 4,
	SceneRenderModeAmbientOcclusion = 5,
	SceneRenderModeCount
};

static RendererSettings sRendererSettings;

static const GLfloat g_vertex_buffer_data[] = {
	-0.9f, -0.9f, 0.0f,
	0.9f, -0.9f, 0.0f,
	0.0f, 0.9f, 4.0f
};

static const GLfloat g_quad_vertex_buffer_data[] = {
	-1.0f, -1.0f, 0.0f,
	1.0f, -1.0f, 0.0f,
	-1.0f, 1.0f, 0.0f,
	-1.0f, 1.0f, 0.0f,
	1.0f, -1.0f, 0.0f,
	1.0f, 1.0f, 0.0f
};

static const GLfloat g_textured_quad_vertex_buffer_data[] = {
	-1.0f, -1.0f, 0.0f,	0.0f, 1.0f,
	1.0f, -1.0f, 0.0f,	1.0f, 1.0f,
	-1.0f, 1.0f, 0.0f,	0.0f, 0.0f,
	-1.0f, 1.0f, 0.0f,	0.0f, 0.0f,
	1.0f, -1.0f, 0.0f,	1.0f, 1.0f,	
	1.0f, 1.0f, 0.0f,		1.0f, 0.0f,
};

VertexArray gVertexArray;
VertexArrayMesh gXZPlaneGrid;
VertexArrayMesh gXZPlaneMesh;
VertexArrayMesh gUnitCubeLines;
VertexArrayMesh gScreenQuad;

AssetFile gAssetFile;

//
// Module
//
struct module_state {
	Renderer *renderer;
};

static struct module_state *module_init() {
	gLog ("%s %s called", __FILE__, __FUNCTION__);
	assert (gWindow != nullptr && "Cannot initialize renderer module without gWindow!");

	module_state *state = (module_state*) malloc(sizeof(*state));
	state->renderer = new Renderer();
	assert (state->renderer != nullptr);

	return state;
}

template <typename Serializer>
static void module_serialize (
		struct module_state *state,
		Serializer* serializer) {
	SerializeBool(*serializer, "protot.RenderModule.DrawLightDepth", sRendererSettings.DrawLightDepth);
	SerializeInt(*serializer, "protot.RenderModule.RenderMode", sRendererSettings.RenderMode);

	SerializeBool(*serializer, "protot.RenderModule.mUseDeferred", gRenderer->mUseDeferred);
	SerializeBool(*serializer, "protot.RenderModule.mIsSSAOEnabled", gRenderer->mIsSSAOEnabled);

	// Camera
	SerializeBool(*serializer, "protot.RenderModule.Camera.mIsOrthographic", gRenderer->mCamera.mIsOrthographic);
	SerializeFloat(*serializer, "protot.RenderModule.Camera.mFov", gRenderer->mCamera.mFov);
	SerializeVec3(*serializer, "protot.RenderModule.Camera.mEye", gRenderer->mCamera.mEye);
	SerializeVec3(*serializer, "protot.RenderModule.Camera.mPoi", gRenderer->mCamera.mPoi);
	SerializeVec3(*serializer, "protot.RenderModule.Camera.mUp", gRenderer->mCamera.mUp);
	SerializeFloat(*serializer, "protot.RenderModule.Camera.mNear", gRenderer->mCamera.mNear);
	SerializeFloat(*serializer, "protot.RenderModule.Camera.mFar", gRenderer->mCamera.mFar);

	// Debug Camera
	SerializeBool(*serializer, "protot.RenderModule.DebugCamera.mIsOrthographic", gRenderer->mDebugCamera.mIsOrthographic);
	SerializeFloat(*serializer, "protot.RenderModule.DebugCamera.mFov", gRenderer->mDebugCamera.mFov);
	SerializeVec3(*serializer, "protot.RenderModule.DebugCamera.mEye", gRenderer->mDebugCamera.mEye);
	SerializeVec3(*serializer, "protot.RenderModule.DebugCamera.mPoi", gRenderer->mDebugCamera.mPoi);
	SerializeVec3(*serializer, "protot.RenderModule.DebugCamera.mUp", gRenderer->mDebugCamera.mUp);
	SerializeFloat(*serializer, "protot.RenderModule.DebugCamera.mNear", gRenderer->mDebugCamera.mNear);
	SerializeFloat(*serializer, "protot.RenderModule.DebugCamera.mFar", gRenderer->mDebugCamera.mFar);

	SerializeVec3(*serializer, "protot.RenderModule.mLight.mPosition", gRenderer->mLight.mPosition);
	SerializeVec3(*serializer, "protot.RenderModule.mLight.mDirection", gRenderer->mLight.mDirection);
	SerializeFloat(*serializer, "protot.RenderModule.mLight.mBBoxSize", gRenderer->mLight.mBBoxSize);
	SerializeFloat(*serializer, "protot.RenderModule.mLight.mNear", gRenderer->mLight.mNear);
	SerializeFloat(*serializer, "protot.RenderModule.mLight.mFar", gRenderer->mLight.mFar);

	SerializeBool(*serializer, "protot.RenderModule.mLight.mDebugDrawSplitViewBounds", gRenderer->mLight.mDebugDrawSplitViewBounds);
	SerializeBool(*serializer, "protot.RenderModule.mLight.mDebugDrawSplitWorldBounds", gRenderer->mLight.mDebugDrawSplitWorldBounds);
	SerializeBool(*serializer, "protot.RenderModule.mLight.mDebugDrawSplitLightBounds", gRenderer->mLight.mDebugDrawSplitLightBounds);
	SerializeVec4 (*serializer, "protot.RenderModule.mLight.mSplitBias", gRenderer->mLight.mSplitBias);
	SerializeVec4 (*serializer, "protot.RenderModule.mLight.mShadowSplits", gRenderer->mLight.mShadowSplits);
}

static void module_finalize(struct module_state *state) {
	gLog ("%s %s called (state %p)", __FILE__, __FUNCTION__, state);

	assert (state->renderer != nullptr);
	delete state->renderer;
	gVertexArray.Cleanup();

	free(state);
}

static void module_reload(struct module_state *state, void *read_serializer) {
	gLog ("%s %s called (state %p)", __FILE__, __FUNCTION__, state);
	assert (gWindow != nullptr);

	gLog ("Renderer initialize");
	assert (state != nullptr);
	state->renderer->Initialize(100, 100);
	state->renderer->mSettings = &sRendererSettings;

	gRenderer = state->renderer;

	// load the state of the module
	if (read_serializer != nullptr) {
		module_serialize(state, static_cast<ReadSerializer*>(read_serializer));
	}
}

static void module_unload(struct module_state *state, void* write_serializer) {
	// serialize the state of the module
	if (write_serializer != nullptr) {
		module_serialize(state, static_cast<WriteSerializer*>(write_serializer));
	}

	gRenderer = nullptr;
	state->renderer->Shutdown();

	gLog ("RenderModule unload called");
}

static bool module_step(struct module_state *state, float dt) {
	int width, height;
	assert (gWindow != nullptr);
	state->renderer->DrawGui();
	state->renderer->RenderGl();
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	return true;
}

extern "C" {

const struct module_api MODULE_API = {
	.init = module_init,
	.finalize = module_finalize,
	.reload = module_reload,
	.unload = module_unload,
	.step = module_step
};
}

void Light::Initialize() {
  gLog("Initializing light");
	mShadowMapProgram = RenderProgram("data/shaders/vs_shadowmap.vert", "data/shaders/fs_shadowmap.frag");

	bool load_result = mShadowMapProgram.Load();
	mShadowMapProgram.RegisterFileModification();
	assert(load_result);

	gLog("Initializing light render target size: %d, %d", mShadowMapSize, mShadowMapSize);
	mShadowMapTarget.Initialize(mShadowMapSize, mShadowMapSize,
			RenderTarget::EnableColor
			| RenderTarget::EnableDepthTexture
			| RenderTarget::EnableLinearizedDepthTexture);

	for (int i = 0; i < cNumSplits; ++i) {
		mSplitTarget[i].Initialize(mShadowMapSize, mShadowMapSize,
				RenderTarget::EnableColor
				| RenderTarget::EnableDepthTexture
				| RenderTarget::EnableLinearizedDepthTexture);
		mSplitTarget[i].mVertexArray = &gVertexArray;
		mSplitTarget[i].mQuadMesh = &gScreenQuad;
	}
}

void Light::UpdateMatrices() {
	mCamera.mIsOrthographic = true;

	mCamera.mEye = mPosition;
	mCamera.mPoi = mPosition + mDirection;
	mCamera.mUp = Vector3f (0.0f, 1.0f, 0.0f);

	mCamera.mProjectionMatrix = Ortho (-mBBoxSize * 0.5f, mBBoxSize * 0.5f, -mBBoxSize * 0.5f, mBBoxSize * 0.5f, mNear, mFar);
	mCamera.mViewMatrix = LookAt (mCamera.mEye, mCamera.mPoi, mCamera.mUp);

	mLightSpaceMatrix = mCamera.mViewMatrix * mCamera.mProjectionMatrix;
}

void Light::DrawGui() {
	ImGui::SliderFloat3("Position", mPosition.data(), -10.0f, 10.0f);
	ImGui::SliderFloat3("Direction", mDirection.data(), -1.0f, 1.0f);
	ImGui::SliderFloat("Near", &mNear, -10.0f, 50.0f);
	ImGui::SliderFloat("Far", &mFar, -10.0f, 50.0f);
	ImGui::SliderFloat4("Shadow Splits Bias", mSplitBias.data(), 0.0f, 0.01f, "%.5f", 1.0f);
	ImGui::SliderFloat4("Shadow Splits", mShadowSplits.data(), 0.01f, 1.0f);

	bool show_cascades = mShowCascadesAlpha == 0.0f ? false : true;
	ImGui::Checkbox("ShowCascades", &show_cascades);
	mShowCascadesAlpha = show_cascades ? 1.0 : 0.0f;

	ImGui::Checkbox("Draw Light Depth", &sRendererSettings.DrawLightDepth);

	ImGui::RadioButton("Split 0", &sRendererSettings.ActiveShadowMapSplit, 0); ImGui::SameLine();
	ImGui::RadioButton("Split 1", &sRendererSettings.ActiveShadowMapSplit, 1); ImGui::SameLine();
	ImGui::RadioButton("Split 2", &sRendererSettings.ActiveShadowMapSplit, 2); ImGui::SameLine();
	ImGui::RadioButton("Split 3", &sRendererSettings.ActiveShadowMapSplit, 3);

	RenderTarget* shadow_split_target = nullptr;
	shadow_split_target = &mSplitTarget[sRendererSettings.ActiveShadowMapSplit];

	void* texture;
	if (sRendererSettings.DrawLightDepth) {
		texture = (void*) shadow_split_target->mLinearizedDepthTexture;
	} else {
		texture = (void*) shadow_split_target->mColorTexture;
	}

	ImVec2 content_avail = ImGui::GetContentRegionAvail();

	ImGui::Image(texture,
			ImVec2(content_avail.x, content_avail.x),
			ImVec2(0.0f, 1.0f),
			ImVec2(1.0f, 0.0f)
			);
}

void Light::UpdateSplits(const Camera& camera) {
	assert(camera.mIsOrthographic == false);

	float near = camera.mNear;
	// Clamp the far plane of the camera so that we only 
	// create shadow maps for things that are relatively near
	// the camera.
	float far = camera.mFar;
	float length = far - near;
	float split_near = near;
	float aspect = camera.mWidth / camera.mHeight;

	float tan_half_hfov = tanf(0.5f * camera.mFov * M_PI / 180.0f);
	float tan_half_vfov = tanf(0.5f * aspect * camera.mFov  * M_PI / 180.0f);

	Vector3f direction = mDirection.normalize();

	Matrix44f look_at = camera.mViewMatrix;
	Matrix44f look_at_inv = look_at.inverse();
	Matrix44f light_matrix = LookAt (mPosition, mPosition + mDirection, Vector3f (0.f, 1.0f, 0.0f));
	Matrix44f light_matrix_inv = light_matrix.inverse();

	mShadowSplits[0] = near + length * 0.03;
	mShadowSplits[1] = near + length * 0.10;
	mShadowSplits[2] = near + length * 0.4;
	mShadowSplits[3] = far;

	float prev_split_far = near;

	for (int i = 0; i < cNumSplits; ++i) {
		split_near = prev_split_far;
		float split_far = mShadowSplits[i];

		mSplitViewFrustum[i] = look_at * Perspective (camera.mFov, aspect, split_near, split_far);

		float xn = split_near * tan_half_vfov;
		float xf = split_far * tan_half_vfov;
		float yn = split_near * tan_half_hfov;
		float yf = split_far * tan_half_hfov;

		Vector4f frustum_corners[] = {
			Vector4f (xn, yn, -split_near, 1.0f),
			Vector4f (-xn, yn, -split_near, 1.0f),
			Vector4f (xn, -yn, -split_near, 1.0f),
			Vector4f (-xn, -yn, -split_near, 1.0f),

			Vector4f (xf, yf, -split_far, 1.0f),
			Vector4f (-xf, yf, -split_far, 1.0f),
			Vector4f (xf, -yf, -split_far, 1.0f),
			Vector4f (-xf, -yf, -split_far, 1.0f)
		};

		BBox bbox_world;
		BBox bbox_light;

		for (int j = 0; j < 8; ++j) {
			Vector4f v_world = look_at_inv.transpose() * frustum_corners[j];
			Vector4f v_light = light_matrix.transpose() * v_world;

			//				gLog("vworld %d: %.3f, %.3f, %.3f, %.3f", j, 
			//						v_world[0],
			//						v_world[1],
			//						v_world[2],
			//						v_world[3]);

			bbox_world.Update(v_world.block<3,1>(0,0));
			bbox_light.Update(v_light.block<3,1>(0,0));
		}

		mSplitBoundsWorldSpace[i] = bbox_world;
		mSplitBoundsLightSpace[i] = bbox_light;

		//			gLog ("min/max %.3f,%.3f, %.3f,%.3f, %.3f,%.3f", 
		//					min_x, max_x,
		//					min_y, max_y,
		//					min_z, max_z
		//					);

		// Compute the light matrices for each frustum
		{
			Vector3f dimensions = (bbox_light.mMax - bbox_light.mMin) * 0.5f;
			Vector3f center = bbox_light.mMin + dimensions;

			mSplitCamera[i].mIsOrthographic = true;
			mSplitCamera[i].mViewMatrix = light_matrix;

			// TODO: values for near and far planes are off
			mSplitCamera[i].mProjectionMatrix = Ortho (
					bbox_light.mMin[0], bbox_light.mMax[0],
					bbox_light.mMin[1], bbox_light.mMax[1],
					//											mLight.mNear, bbox_light.mMax[2]
										mNear, mFar
					//						bbox_light.mMin[2], mLight.mFar
//					bbox_light.mMin[2], bbox_light.mMax[2]
					);

			mSplitLightFrustum[i] = mSplitCamera[i].mViewMatrix
				* mSplitCamera[i].mProjectionMatrix;
			
			prev_split_far = split_far;
		}
	}
}


//
// Renderer
//
void Renderer::Initialize(int width, int height) {
	DebugDrawInitialize();

	mDefaultTexture.MakeGrid(128, Vector3f (0.8, 0.8f, 0.8f), Vector3f (0.7f, 0.7f, 0.7f));

	gVertexArray.Initialize(1000, GL_STATIC_DRAW);

	// Plane Grid
	const int plane_grid_size = 20;
	gXZPlaneGrid.Initialize(gVertexArray, (plane_grid_size + 1) * 4);

	std::vector<VertexArray::VertexData> plane_data((plane_grid_size + 1) * 4);
	for (int i = 0, n = plane_grid_size + 1; i < n; ++i) {
		// lines along the x axis
		plane_data[2 * i] = VertexArray::VertexData(
				- plane_grid_size * 0.5f, 0.0f, -plane_grid_size * 0.5f + 1.0f * i, 1.0f,
				0.0f, 1.0f, 0.0f,
				- plane_grid_size * 0.5f, -1.0f * i,
				255, 255, 255, 255);
		plane_data[2 * i + 1] = VertexArray::VertexData(
				plane_grid_size * 0.5f, 0.0f, -plane_grid_size * 0.5 + 1.0f * i, 1.0f,
				0.0f, 1.0f, 0.0f,
				plane_grid_size * 0.5f, -1.0f * i,
				255, 255, 255, 255);

		// lines along the z axis
		plane_data[n * 2 + 2 * i] = VertexArray::VertexData(
				 -plane_grid_size * 0.5f + 1.0f * i, 0.0f, - plane_grid_size * 0.5f, 1.0f,
				0.0f, 1.0f, 0.0f,
				 -1.0f * i, - plane_grid_size * 0.5f,
				255, 255, 255, 255);
		plane_data[n * 2 + 2 * i + 1] = VertexArray::VertexData(
				 -plane_grid_size * 0.5f + 1.0f * i, 0.0f,   plane_grid_size * 0.5f, 1.0f,
				0.0f, 1.0f, 0.0f,
				 -1.0f * i, plane_grid_size * 0.5f,
				255, 255, 255, 255);
	}
	gXZPlaneGrid.SetData(plane_data.data(), plane_data.size());

	// Plane Mesh
	gXZPlaneMesh.Initialize(gVertexArray, 4);
	VertexArray::VertexData plane_mesh_vertex_data[] = {
		{-10.0f, 0.0f, -10.0f, 1.0f,		0.0f, 1.0f, 0.0f,		-5.0f, -5.0f,		255, 255, 255, 255},
		{-10.0f, 0.0f,  10.0f, 1.0f,		0.0f, 1.0f, 0.0f,		-5.0f,  5.0f,		255, 255, 255, 255},
		{ 10.0f, 0.0f,  10.0f, 1.0f,		0.0f, 1.0f, 0.0f,	 	 5.0f,  5.0f,		255, 255, 255, 255},
		{ 10.0f, 0.0f, -10.0f, 1.0f,		0.0f, 1.0f, 0.0f,		 5.0f, -5.0f,		255, 255, 255, 255}
	};
	gXZPlaneMesh.SetData(plane_mesh_vertex_data, 4);
	GLuint xz_plane_index_data[] = {
		0, 1, 2,
		2, 3, 0
	};
	gXZPlaneMesh.SetIndexData(xz_plane_index_data, 6);

	// Screen Quad
	gScreenQuad.Initialize(gVertexArray, 4);
	VertexArray::VertexData screen_quad_data[] = {
		{-1.0f, -1.0f, 0.0f, 1.0f,	0.0f, 0.0f, 1.0f,		0.0f, 0.0f,		255, 255, 255, 255},
		{ 1.0f, -1.0f, 0.0f, 1.0f,	0.0f, 0.0f, 1.0f,		1.0f, 0.0f,		255, 255, 255, 255},
		{ 1.0f,  1.0f, 0.0f, 1.0f,	0.0f, 0.0f, 1.0f,		1.0f, 1.0f,		255, 255, 255, 255},
		{-1.0f,  1.0f, 0.0f, 1.0f,	0.0f, 0.0f, 1.0f,		0.0f, 1.0f,		255, 255, 255, 255}
	};
	gScreenQuad.SetData(screen_quad_data, 4);
	GLuint screen_quad_indices[] = {
		0, 1, 2,
		2, 3, 0
	};
	gScreenQuad.SetIndexData(screen_quad_indices, 6);

	// Simple Shader
	mSimpleProgram = RenderProgram("data/shaders/vs_simple.glsl", "data/shaders/fs_simple.glsl");
	bool load_result = mSimpleProgram.Load();
	mSimpleProgram.RegisterFileModification();
	assert(load_result);

	mDefaultProgram = RenderProgram("data/shaders/vs_default.glsl", "data/shaders/fs_default.glsl");
	load_result = mDefaultProgram.Load();
	mDefaultProgram.RegisterFileModification();
	assert(load_result);

	// Program for color texture rendering
	mRenderQuadProgramColor = RenderProgram("data/shaders/vs_passthrough.glsl", "data/shaders/fs_simpletexture.glsl");
	load_result = mRenderQuadProgramColor.Load();
	mRenderQuadProgramColor.RegisterFileModification();
	assert(load_result);
		
	// Program for depth texture rendering
	mRenderQuadProgramDepth = RenderProgram("data/shaders/vs_passthrough.glsl", "data/shaders/fs_depthbuffer.glsl");
	load_result = mRenderQuadProgramDepth.Load();
	mRenderQuadProgramDepth.RegisterFileModification();
	assert(load_result);

	// Deferred geomemtry pass
	mDeferredGeometry = RenderProgram("data/shaders/vs_deferred_geometry.glsl", "data/shaders/fs_deferred_geometry.glsl");
	load_result = mDeferredGeometry.Load();
	mDeferredGeometry.RegisterFileModification();
	assert(load_result);

	mDeferredLighting = RenderProgram("data/shaders/vs_passthrough.glsl", "data/shaders/fs_deferred_lighting.glsl");
	load_result = mDeferredLighting.Load();
	mDeferredLighting.RegisterFileModification();
	assert(load_result);

	// Program for SSAO
	mSSAOProgram = RenderProgram("data/shaders/vs_passthrough.glsl", "data/shaders/fs_ssao.glsl");
	load_result = mSSAOProgram.Load();
	mSSAOProgram.RegisterFileModification();
	assert(load_result);

	mBlurSSAOProgram = RenderProgram("data/shaders/vs_passthrough.glsl", "data/shaders/fs_blur_ssao.glsl");
	load_result = mBlurSSAOProgram.Load();
	mBlurSSAOProgram.RegisterFileModification();
	assert(load_result);

	InitializeSSAOKernelAndNoise();

	// Render Target
	gLog("Initializing main render target size: %d,%d", width, height);
	int render_target_flags = RenderTarget::EnableColor 
			| RenderTarget::EnableDepthTexture 
			| RenderTarget::EnableLinearizedDepthTexture;

	if (mIsSSAOEnabled) {
		render_target_flags = render_target_flags
			| RenderTarget::EnablePositionTexture
			| RenderTarget::EnableNormalTexture;
	}
	mForwardRenderingTarget.Initialize(width, height, render_target_flags);

	mForwardRenderingTarget.mVertexArray = &gVertexArray;
	mForwardRenderingTarget.mQuadMesh = &gScreenQuad;
	mForwardRenderingTarget.mLinearizeDepthProgram = mRenderQuadProgramDepth;
	mForwardRenderingTarget.mLinearizeDepthProgram.RegisterFileModification();

	// SSAO Target
	mSSAOTarget.Initialize(width, height, RenderTarget::EnableColor);
	mSSAOBlurTarget.Initialize(width, height, RenderTarget::EnableColor);

	mDeferredLightingTarget.Initialize(width, height, RenderTarget::EnableColor);

	mRenderOutput.Initialize(width, height, RenderTarget::EnableColor | RenderTarget::EnableDepth);

	// Light
	mLight.Initialize();
	mLight.mShadowMapTarget.mVertexArray = &gVertexArray;
	mLight.mShadowMapTarget.mQuadMesh = &gScreenQuad;
	mLight.mShadowMapTarget.mLinearizeDepthProgram = mRenderQuadProgramDepth;
	mLight.mShadowMapTarget.mLinearizeDepthProgram.RegisterFileModification();

	for (int i = 0; i < cNumSplits; ++i) {
		mLight.mSplitTarget[i].mLinearizeDepthProgram = mRenderQuadProgramDepth;
		mLight.mSplitTarget[i].mLinearizeDepthProgram.RegisterFileModification();
	}

	// Model
	if (!gAssetFile.Load("data/models/RiggedFigure/glTF/RiggedFigure.gltf")) {
		assert(false);
	}
}

void Renderer::Shutdown() {
	if (mSSAONoiseTexture != -1) {
		glDeleteTextures(1, &mSSAONoiseTexture);
	}
}

void Renderer::CheckRenderBuffers() {
	mSceneAreaWidth = mSceneAreaWidth < 1 ? 1 : mSceneAreaWidth;
	mSceneAreaHeight = mSceneAreaHeight < 1 ? 1 : mSceneAreaHeight;

	// TODO: Refactor enabling/disabling buffers for SSAO
	int required_render_flags = RenderTarget::EnableColor 
		| RenderTarget::EnableDepthTexture 
		| RenderTarget::EnableLinearizedDepthTexture;

	if (mIsSSAOEnabled) {
		required_render_flags = required_render_flags
			| RenderTarget::EnablePositionTexture
			| RenderTarget::EnableNormalTexture;
	}

	if (mUseDeferred) {
		required_render_flags = RenderTarget::EnableColor
			| RenderTarget::EnableDepthTexture
			| RenderTarget::EnableNormalTexture

			// TODO: remove these
			| RenderTarget::EnablePositionTexture
			| RenderTarget::EnableLinearizedDepthTexture
			;
	}

	if (mSceneAreaWidth != mRenderOutput.mWidth
			|| mSceneAreaHeight != mRenderOutput.mHeight
			|| mForwardRenderingTarget.mFlags != required_render_flags ) {
		mRenderOutput.Resize(mSceneAreaWidth, mSceneAreaHeight, RenderTarget::EnableColor);
		mForwardRenderingTarget.Resize(mSceneAreaWidth, mSceneAreaHeight, required_render_flags);
		mDeferredLightingTarget.Resize(mSceneAreaWidth, mSceneAreaHeight,
                                       RenderTarget::EnableColor
                                       | RenderTarget::EnableDepthTexture
                                       | RenderTarget::EnableNormalTexture

                                       // TODO: remove these
                                       | RenderTarget::EnablePositionTexture
                                       | RenderTarget::EnableLinearizedDepthTexture);
	}

	if (mIsSSAOEnabled
			&& (mSSAOTarget.mWidth != mSceneAreaWidth
				|| mSSAOTarget.mHeight != mSceneAreaHeight)) {
		mSSAOTarget.Resize(mSceneAreaWidth, mSceneAreaHeight, RenderTarget::EnableColor);
		mSSAOBlurTarget.Resize(mSceneAreaWidth, mSceneAreaHeight, RenderTarget::EnableColor);
	}
}

void Renderer::RenderGl() {
	CheckRenderBuffers();

	if (mCamera.mWidth != mSceneAreaWidth
			|| mCamera.mHeight != mSceneAreaHeight) {
		mCamera.mWidth = mSceneAreaWidth;
		mCamera.mHeight = mSceneAreaHeight;
		mDebugCamera.mWidth = mSceneAreaWidth;
		mDebugCamera.mHeight = mSceneAreaHeight;
	}

	mDebugCamera.mWidth = 300.0f;
	mDebugCamera.mHeight = 300.0f;
	mDebugCamera.UpdateMatrices();

	mCamera.UpdateMatrices();
	mLight.UpdateMatrices();
	mLight.UpdateSplits(mCamera);

	// Cascaded Shadow Maps
	for (int i = 0; i < cNumSplits; i++) {

		mLight.mSplitTarget[i].Bind();
		glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
		glEnable(GL_DEPTH_TEST);

		glUseProgram(mLight.mShadowMapProgram.mProgramId);
		glViewport(0, 0, mLight.mShadowMapSize, mLight.mShadowMapSize);

		if (mLight.mShadowMapProgram.SetMat44("uViewProjectionMatrix", mLight.mSplitLightFrustum[i]) == -1) {
			gLog ("Warning: Uniform %s not found!", "uViewProjectionMatrix");
		}
		
		RenderScene(mLight.mShadowMapProgram, mLight.mSplitCamera[i]);
		mLight.mSplitTarget[i].RenderToLinearizedDepth(mLight.mCamera.mNear, mLight.mCamera.mFar, true);
	}

	// Shadow Map
	mLight.mShadowMapTarget.Bind();
		glViewport(0, 0, mLight.mShadowMapSize, mLight.mShadowMapSize);
		mLight.mShadowMapTarget.Bind();
		glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
		glEnable(GL_DEPTH_TEST);

		glUseProgram(mLight.mShadowMapProgram.mProgramId);
		glViewport(0, 0, mLight.mShadowMapSize, mLight.mShadowMapSize);

		if (mLight.mShadowMapProgram.SetMat44("uViewProjectionMatrix", mLight.mLightSpaceMatrix) == -1) {
			gLog ("Warning: Uniform %s not found!", "uViewProjectionMatrix");
		}
		
		RenderScene(mLight.mShadowMapProgram, mLight.mCamera);
		mLight.mShadowMapTarget.RenderToLinearizedDepth(mLight.mCamera.mNear, mLight.mCamera.mFar, true);

	glViewport(0, 0, mCamera.mWidth, mCamera.mHeight);

	Matrix44f model_matrix = TranslateMat44(0.0f, 0.0f, 0.0f);
	Matrix44f model_view_projection = 
		model_matrix
		* mCamera.mViewMatrix
		* mCamera.mProjectionMatrix;

  // Clear the SSAO Blur target
  if (!mIsSSAOEnabled) {
    mSSAOBlurTarget.Bind();
    GLenum draw_attachment_0[] = {GL_COLOR_ATTACHMENT0 };
    glDrawBuffers(1, draw_attachment_0);
    glClearColor(1.f, 1.f, 1.f, 1.f);
    glClear(GL_COLOR_BUFFER_BIT);
    glDisable(GL_DEPTH_TEST);
 }

  // enable the render target
	mForwardRenderingTarget.Bind();
  GLenum draw_attachment_0[] = {GL_COLOR_ATTACHMENT0 };
  glDrawBuffers(1, draw_attachment_0);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	glEnable(GL_DEPTH_TEST);
	RenderProgram *program = &mDefaultProgram;
	if (mUseDeferred) {
		program = &mDeferredGeometry;
		
		GLenum buffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 };
		glDrawBuffers(3, buffers);
		glClear(GL_COLOR_BUFFER_BIT);
	} else {
		GLenum buffers[] = { GL_COLOR_ATTACHMENT0};
		glDrawBuffers (1, buffers);
	}

	// clear color and depth
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glEnable(GL_DEPTH_TEST);
	glDisable(GL_LINE_SMOOTH);
	glHint(GL_LINE_SMOOTH_HINT, GL_NICEST);
	glEnable(GL_MULTISAMPLE);
	glEnable(GL_CULL_FACE);

	glUseProgram(mSimpleProgram.mProgramId);
	mSimpleProgram.SetMat44("uModelViewProj", model_view_projection);
	mSimpleProgram.SetVec4("uColor", Vector4f (1.0f, 0.0f, 0.0f, 1.0f));

	// Coordinate System: VertexArrayMesh
	model_view_projection = 
		TranslateMat44(0.0f, 0.002f, 0.0f)
		* mCamera.mViewMatrix
		* mCamera.mProjectionMatrix;
	mSimpleProgram.SetMat44("uModelViewProj", model_view_projection);
	mSimpleProgram.SetVec4("uColor", Vector4f (0.0f, 0.0f, 0.0f, 1.0f));

	DebugDrawFrame(
			mSimpleProgram,
		TranslateMat44(1.0f, 0.002f, 0.0f)
		* mCamera.mViewMatrix
		* mCamera.mProjectionMatrix
		);

	gVertexArray.Bind();

	// Plane
	model_view_projection = 
		TranslateMat44(0.0f, 0.001f, 0.0f)
		* mCamera.mViewMatrix
		* mCamera.mProjectionMatrix;
	mSimpleProgram.SetMat44("uModelViewProj", model_view_projection);
	mSimpleProgram.SetVec4("uColor", Vector4f (1.0f, 1.0f, 1.0f, 0.1f));
	gVertexArray.Bind();
	gXZPlaneGrid.Draw(GL_LINES);

	// Scene
	glUseProgram(program->mProgramId);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, mLight.mShadowMapTarget.mDepthTexture);
	program->SetInt("uShadowMap",  0);

	if (!mUseDeferred) {
		// Shadow Map Cascades
		glActiveTexture(GL_TEXTURE5);
		glBindTexture(GL_TEXTURE_2D, mLight.mSplitTarget[0].mDepthTexture);
		glActiveTexture(GL_TEXTURE6);
		glBindTexture(GL_TEXTURE_2D, mLight.mSplitTarget[1].mDepthTexture);
		glActiveTexture(GL_TEXTURE7);
		glBindTexture(GL_TEXTURE_2D, mLight.mSplitTarget[2].mDepthTexture);
		glActiveTexture(GL_TEXTURE8);
		glBindTexture(GL_TEXTURE_2D, mLight.mSplitTarget[3].mDepthTexture);

		GLint shadow_maps[cNumSplits];
		shadow_maps[0] = 5;
		shadow_maps[1] = 6;
		shadow_maps[2] = 7;
		shadow_maps[3] = 8;
		program->SetIntArray("uShadowMaps", cNumSplits, shadow_maps);

		Matrix44f light_matrices[cNumSplits];

		for (int i = 0; i < cNumSplits; ++i) {
			light_matrices[i] = mCamera.mViewMatrix.inverse() 
				* mLight.mSplitLightFrustum[i];
		}
		program->SetFloat("uNear", mCamera.mNear);
		program->SetFloat("uFar", mCamera.mFar);
		program->SetVec4("uShadowSplits", mLight.mShadowSplits);
		program->SetVec4("uShadowSplitBias", mLight.mSplitBias);
		program->SetFloat("uShowCascadesAlpha", mLight.mShowCascadesAlpha);


		program->SetMat44Array("uViewToLightMatrix", cNumSplits, light_matrices);
		program->SetMat44("uLightSpaceMatrix", mLight.mLightSpaceMatrix); 
	}

	RenderScene(*program, mCamera);

	if (mSettings->RenderMode == SceneRenderModeDepth) {
		mForwardRenderingTarget.RenderToLinearizedDepth(mCamera.mNear, mCamera.mFar, mCamera.mIsOrthographic);
	}

	if (mIsSSAOEnabled) {
		mSSAOTarget.Bind();
		glViewport(0, 0, mCamera.mWidth, mCamera.mHeight);
		GLenum draw_attachment_0[] = {GL_COLOR_ATTACHMENT0 };
		glDrawBuffers(1, draw_attachment_0);
		glClear(GL_COLOR_BUFFER_BIT);
		glDisable(GL_DEPTH_TEST);
	
		// Positions
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, mForwardRenderingTarget.mPositionTexture);

		// Normals
		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, mForwardRenderingTarget.mNormalTexture);

		// Noise
		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, mSSAONoiseTexture);

		glUseProgram(mSSAOProgram.mProgramId);

		mSSAOProgram.SetInt("uPositions", 0);
		mSSAOProgram.SetInt("uNormals", 1);
		mSSAOProgram.SetInt("uNoise", 2);

		mSSAOProgram.SetFloat("uRadius", mSSAORadius);
		mSSAOProgram.SetFloat("uBias", mSSAOBias);
		mSSAOProgram.SetFloat("uPower", mSSAOPower);
		mSSAOProgram.SetInt("uSampleCount", mSSAOKernel.size());
		mSSAOProgram.SetMat44("uProjection", mCamera.mProjectionMatrix);

		mSSAOProgram.SetVec3Array("uSamples", mSSAOKernel.size(), &mSSAOKernel[0][0]);

		gVertexArray.Bind();
		gScreenQuad.Draw(GL_TRIANGLES);

		// Blur pass
		mSSAOBlurTarget.Bind();
		glViewport(0, 0, mCamera.mWidth, mCamera.mHeight);
		glDrawBuffers(1, draw_attachment_0);
		glClearColor(1.f, 1.f, 1.f, 1.f);
		glClear(GL_COLOR_BUFFER_BIT);
		glDisable(GL_DEPTH_TEST);
		
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, mSSAOTarget.mColorTexture);

		glUseProgram(mBlurSSAOProgram.mProgramId);
		mBlurSSAOProgram.SetInt("uAmbientOcclusion", 0);
		mBlurSSAOProgram.SetInt("uBlurSize", mSSAOBlurSize);

		gVertexArray.Bind();
		gScreenQuad.Draw(GL_TRIANGLES);
	}

	if (mUseDeferred) {
		// Deferred: Lighting pass
		GLenum buffers[] = { GL_COLOR_ATTACHMENT0};
		glDrawBuffers (1, buffers);

		mDeferredLightingTarget.Bind();
		glClearColor(0, 0, 0, 255);
		glClear(GL_COLOR_BUFFER_BIT);
	
		glUseProgram(mDeferredLighting.mProgramId);

		// TODO: remove and reconstruct position from depth
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, mForwardRenderingTarget.mPositionTexture);
		mDeferredLighting.SetInt("uPosition", 0);

		glActiveTexture(GL_TEXTURE1);
		glBindTexture(GL_TEXTURE_2D, mForwardRenderingTarget.mDepthTexture);
		mDeferredLighting.SetInt("uDepth", 1);

		glActiveTexture(GL_TEXTURE2);
		glBindTexture(GL_TEXTURE_2D, mForwardRenderingTarget.mNormalTexture);
		mDeferredLighting.SetInt("uNormal", 2);

		glActiveTexture(GL_TEXTURE3);
		glBindTexture(GL_TEXTURE_2D, mForwardRenderingTarget.mColorTexture);
		mDeferredLighting.SetInt("uColor", 3);

		glActiveTexture(GL_TEXTURE4);
		glBindTexture(GL_TEXTURE_2D, mSSAOBlurTarget.mColorTexture);
		mDeferredLighting.SetInt("uAmbientOcclusion", 4);

		// Shadow Map Cascades
		glActiveTexture(GL_TEXTURE5);
		glBindTexture(GL_TEXTURE_2D, mLight.mSplitTarget[0].mDepthTexture);
		glActiveTexture(GL_TEXTURE6);
		glBindTexture(GL_TEXTURE_2D, mLight.mSplitTarget[1].mDepthTexture);
		glActiveTexture(GL_TEXTURE7);
		glBindTexture(GL_TEXTURE_2D, mLight.mSplitTarget[2].mDepthTexture);
		glActiveTexture(GL_TEXTURE8);
		glBindTexture(GL_TEXTURE_2D, mLight.mSplitTarget[3].mDepthTexture);

		GLint shadow_maps[cNumSplits];
		shadow_maps[0] = 5;
		shadow_maps[1] = 6;
		shadow_maps[2] = 7;
		shadow_maps[3] = 8;
		mDeferredLighting.SetIntArray("uShadowMap", cNumSplits, shadow_maps);

		Matrix44f light_matrices[cNumSplits];

		for (int i = 0; i < cNumSplits; ++i) {
			light_matrices[i] = mCamera.mViewMatrix.inverse() 
				* mLight.mSplitLightFrustum[i];
		}

		mDeferredLighting.SetMat44Array("uViewToLightMatrix", cNumSplits, light_matrices);

		mDeferredLighting.SetFloat("uNear", mCamera.mNear);
		mDeferredLighting.SetFloat("uFar", mCamera.mFar);
		mDeferredLighting.SetVec4("uShadowSplits", mLight.mShadowSplits);
		mDeferredLighting.SetVec4("uShadowSplitBias", mLight.mSplitBias);
		mDeferredLighting.SetFloat("uShowCascadesAlpha", mLight.mShowCascadesAlpha);

		mDeferredLighting.SetMat44("uViewMatrix", mCamera.mViewMatrix);
		Matrix33f view_mat_rot = mCamera.mViewMatrix.block<3,3>(0,0);
		view_mat_rot = view_mat_rot.transpose();
		Vector3f light_direction = view_mat_rot * mLight.mDirection.normalized();

		mDeferredLighting.SetVec3("uLightDirection", light_direction.normalized());
		mDeferredLighting.SetFloat("uShadowBias", mLight.mShadowBias);

		glDisable(GL_DEPTH_TEST);
		gVertexArray.Bind();
		gScreenQuad.Draw(GL_TRIANGLES);
	}

	if (mDrawDebugCamera) {
		DebugDrawShadowCascades();
	}
}

void Renderer::DebugDrawShadowCascades() {
		GLenum buffers[] = { GL_COLOR_ATTACHMENT0};
		glDrawBuffers (1, buffers);

		mDeferredLightingTarget.Bind();

		glUseProgram(mSimpleProgram.mProgramId);
		// Enable wireframe
		glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
		glDisable(GL_DEPTH_TEST);
		glDisable(GL_CULL_FACE);

		gVertexArray.Bind();
		glUseProgram(mSimpleProgram.mProgramId);

		for (int i = 0; i < cNumSplits; ++i) {
			const BBox& bbox_light = mLight.mSplitBoundsLightSpace[i];
			const BBox& bbox_world = mLight.mSplitBoundsWorldSpace[i];

			// Draw view split frustum
			if (mLight.mDebugDrawSplitViewBounds) {
				Matrix44f model_view_projection =
					mLight.mSplitViewFrustum[i].inverse()
					* mCamera.mViewMatrix
					* mCamera.mProjectionMatrix;

				mSimpleProgram.SetMat44("uModelViewProj", model_view_projection);
				mSimpleProgram.SetVec4("uColor", Vector4f (1.0f, 0.0f, 1.0f, 1.0f));
				gUnitCubeLines.Draw(GL_LINES);
			}

			// Draw bounding boxes in world space 
			if (mLight.mDebugDrawSplitWorldBounds) {
				Vector3f dimensions = (bbox_world.mMax - bbox_world.mMin) * 0.5f;
				Vector3f center = bbox_world.mMin + dimensions;

				Matrix44f model_view_projection = 
					ScaleMat44 (dimensions[0], dimensions[1], dimensions[2])
					* TranslateMat44(center[0], center[1], center[2])
					* mCamera.mViewMatrix
					* mCamera.mProjectionMatrix;

				mSimpleProgram.SetMat44("uModelViewProj", model_view_projection);
				mSimpleProgram.SetVec4("uColor", Vector4f (0.0f, 1.0f, 1.0f, 1.0f));
				gUnitCubeLines.Draw(GL_LINES);
			}

			// Draw bounding boxes in light space
			if (mLight.mDebugDrawSplitLightBounds) {
				Matrix44f model_view_projection = 
					mLight.mSplitLightFrustum[i].inverse()
					* mCamera.mViewMatrix
					* mCamera.mProjectionMatrix;

				mSimpleProgram.SetMat44("uModelViewProj", model_view_projection);
				mSimpleProgram.SetVec4("uColor", Vector4f (0.0f, 0.0f, 1.0f, 1.0f));
				gUnitCubeLines.Draw(GL_LINES);
			}
		}

		// Disable wireframe
		glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
		glEnable(GL_CULL_FACE);
		glEnable(GL_DEPTH_TEST);
}

void Renderer::SubmitRenderCommands (RenderProgram &program, const Camera &camera) {
	glUseProgram(program.mProgramId);

	program.SetMat44("uViewMatrix", camera.mViewMatrix);
	program.SetMat44("uProjectionMatrix", camera.mProjectionMatrix);

 	program.SetVec4("uColor", Vector4f (1.0f, 1.0f, 1.0f, 1.0f));
	program.SetVec3("uLightDirection", mLight.mDirection);
	program.SetVec3("uViewPosition", camera.mEye);
	
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, mDefaultTexture.mTextureId);
	program.SetInt("uAlbedoTexture",  1);

	for (int i = 0, n = mRenderCommands.size(); i < n; ++i) {
		const RenderCommand& command = mRenderCommands[i];

		Matrix44f transform;
		memcpy (transform.data(), command.mTransform, sizeof(float) * 16);
		Vector4f color;
		memcpy (color.data(), command.mColor, sizeof(float) * 4);
	 	program.SetVec4("uColor", color);

		program.SetMat44("uModelMatrix", transform);
		Matrix33f normal_matrix = transform.block<3,3>(0,0).transpose();
		normal_matrix = normal_matrix.inverse();
		program.SetMat33("uNormalMatrix", normal_matrix);

		if (command.mObject == DebugCube) {
			DebugDrawCube(program,
				transform
				);
		} else if (command.mObject == DebugSphere) {
			DebugDrawSphere(program,
				transform,
				color
				);
		} else if (command.mObject == DebugFrame) {
			DebugDrawFrame(program,
				transform
				);
		} else if (command.mObject == DebugBone) {
			DebugDrawBone(program,
				transform,
				color
				);
		}
	}
}


void Renderer::RenderScene(RenderProgram &program, const Camera& camera) {
	SubmitRenderCommands(program, camera);

	glUseProgram(program.mProgramId);

	Matrix44f model_matrix = TranslateMat44(3.0f, 0.0f, 1.0f);
	Matrix33f normal_matrix = model_matrix.block<3,3>(0,0).transpose();
	normal_matrix = normal_matrix.inverse();

	if (program.SetMat44("uModelMatrix", model_matrix) == -1) {
		gLog ("Warning: could not find uModelMatrix");
	}
	program.SetMat44("uViewMatrix", camera.mViewMatrix);
	program.SetMat44("uProjectionMatrix", camera.mProjectionMatrix);
	program.SetMat33("uNormalMatrix", normal_matrix);

 	program.SetVec4("uColor", Vector4f (1.0f, 1.0f, 1.0f, 1.0f));
	program.SetVec3("uLightDirection", mLight.mDirection);
	program.SetVec3("uViewPosition", camera.mEye);
	
	program.SetMat44("uLightSpaceMatrix", mLight.mLightSpaceMatrix); 

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, mLight.mShadowMapTarget.mDepthTexture);
	program.SetInt("uShadowMap",  0);

	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, mDefaultTexture.mTextureId);
	program.SetInt("uAlbedoTexture",  1);

	gVertexArray.Bind();

	DebugDrawCube(
			program,
			RotateMat44(sin(0.3 * gTimer->mCurrentTime) * 180.0f, 0.0f, 1.0f, 0.0f)
			* TranslateMat44(5.0, 1.0 + sin(2.0f * gTimer->mCurrentTime), 0.0)
			);

	DebugDrawCube(
			program,
			RotateMat44(60.0f, 0.0f, 1.0f, 0.0f)
			* TranslateMat44(-4.0f, 1.0f, 4.0f)
			* ScaleMat44(0.5f, 0.5f, 0.5f)
			);

	DebugDrawCube(
			program,
			RotateMat44(60.0f, 0.0f, 1.0f, 0.0f)
			* TranslateMat44(4.0f, 1.0f, 5.0f)
			* ScaleMat44(0.8f, 0.8f, 0.8f)
			);

	DebugDrawCube(
			program,
			RotateMat44(200.0f, 0.0f, 1.0f, 0.0f)
			* TranslateMat44(moving_factor * sin(gTimer->mCurrentTime), 1.0f, -3.0f)
			* ScaleMat44(0.5f, 0.5f, 0.5f)
			);

	gVertexArray.Bind();

	program.SetMat44("uModelMatrix", Matrix44f::Identity());
	program.SetVec4("uColor", Vector4f (1.0f, 1.0f, 1.0f, 1.0f));
	gXZPlaneMesh.Draw(GL_TRIANGLES);

//	DebugDrawSphere(
//			program,
//			TranslateMat44(0.0f, 2.0f, 2.0f),
//			Vector3f (0.4f, 0.2f, 0.9f)
//			);


	DebugDrawBone(
			program,
			TranslateMat44(1.0f, 0.0f, 0.0f)
			);

	gAssetFile.DrawModel(program);
}

void Renderer::DrawGui() {
	if (ImGui::Begin("Scene")) {
		ImGui::RadioButton("Default", &sRendererSettings.RenderMode, 0); ImGui::SameLine();
		ImGui::RadioButton("Color", &sRendererSettings.RenderMode, 1); ImGui::SameLine();
		ImGui::RadioButton("Depth", &sRendererSettings.RenderMode, 2); ImGui::SameLine();
		ImGui::RadioButton("Normals", &sRendererSettings.RenderMode, 3); ImGui::SameLine();
		ImGui::RadioButton("Positions", &sRendererSettings.RenderMode, 4); ImGui::SameLine();
		ImGui::RadioButton("AO", &sRendererSettings.RenderMode, 5);

    if (mUseDeferred) {
      glBindFramebuffer(GL_READ_FRAMEBUFFER, mDeferredLightingTarget.mFrameBufferId);
    } else {
      glBindFramebuffer(GL_READ_FRAMEBUFFER, mForwardRenderingTarget.mFrameBufferId);
    }

    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, mRenderOutput.mFrameBufferId);

    glBlitFramebuffer(0, 0, mSceneAreaWidth, mSceneAreaHeight,
                      0, 0, mSceneAreaWidth, mSceneAreaHeight,
                      GL_COLOR_BUFFER_BIT, GL_NEAREST);

    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);

		switch (sRendererSettings.RenderMode) {
			case SceneRenderModeDefault:
        mRenderTextureRef.mTextureIdPtr = &mRenderOutput.mColorTexture;
				break;
			case SceneRenderModeColor:
				mRenderTextureRef.mTextureIdPtr = &mForwardRenderingTarget.mColorTexture;
				break;
			case SceneRenderModeDepth:
				mRenderTextureRef.mTextureIdPtr = &mForwardRenderingTarget.mLinearizedDepthTexture;
				break;
			case SceneRenderModeNormals:
				mRenderTextureRef.mTextureIdPtr = &mForwardRenderingTarget.mNormalTexture;
				break;
			case SceneRenderModePositions:
				mRenderTextureRef.mTextureIdPtr = &mForwardRenderingTarget.mPositionTexture;
				break;
			case SceneRenderModeAmbientOcclusion:
				mRenderTextureRef.mTextureIdPtr = &mSSAOBlurTarget.mColorTexture;
				break;
		}

		ImGui::Text("Scene");
		const ImVec2 content_avail = ImGui::GetContentRegionAvail();
		mSceneAreaWidth = content_avail.x;
		mSceneAreaHeight = content_avail.y;

		mRenderTextureRef.magic = (GLuint)0xbadface;
		ImGui::Image((void*) &mRenderTextureRef,
				content_avail,
				ImVec2(0.0f, 1.0f), 
				ImVec2(1.0f, 0.0f)
				);
	}
	
	ImGui::End();

	if (ImGui::Begin("Light Settings")) {
		mLight.DrawGui();
	}
	ImGui::End();

	if (ImGui::Begin("Render Settings")) {
		ImGui::Text("Camera");

		mCamera.DrawGui();

		ImGui::Checkbox("Enable Deferred", &mUseDeferred);
		ImGui::Checkbox("Enable SSAO", &mIsSSAOEnabled);

		if (mIsSSAOEnabled) {
			ImGui::SliderFloat("Radius", &mSSAORadius, 0.0f, 1.0f);
			ImGui::SliderFloat("Bias", &mSSAOBias, 0.0f, 0.1f);
			ImGui::SliderFloat("Power", &mSSAOPower, 1.0f, 10.0f);
			ImGui::SliderInt("Samples", &mSSAOKernelSize, 1, 64);
			ImGui::SliderInt("Blur Size", &mSSAOBlurSize, 0, 8);
			ImGui::Checkbox("Disable Color", &mSSAODisableColor);

			if (mSSAOKernelSize != mSSAOKernel.size()) {
				InitializeSSAOKernelAndNoise();
			}
		}
	}
	ImGui::End();

	if (ImGui::Begin("Asset")) {
		gAssetFile.DrawGui();
	}
	ImGui::End();
}

void Renderer::InitializeSSAOKernelAndNoise() {
	mSSAOKernel.clear();

	for (unsigned int i = 0; i < mSSAOKernelSize; ++i) {
		Vector3f sample(
				gRandomFloat() * 2.0f - 1.0f,
				gRandomFloat() * 2.0f - 1.0f,
				gRandomFloat()
				);
		sample.normalize();

		// Have a higher distribution of samples close to the origin
		float scale = (float) i / mSSAOKernelSize;
		scale = 0.1 + scale * scale * (1.0f - 0.1);

		mSSAOKernel.push_back(sample * scale);
	}

	std::vector<Vector3f> noise_vectors;
	for (unsigned int i = 0; i < 16; ++i) {
		Vector3f noise(
				gRandomFloat() * 2.0f - 1.0f,
				gRandomFloat() * 2.0f - 1.0f,
				0.0f);
		noise_vectors.push_back(noise);
	}

	if (mSSAONoiseTexture != -1) {
		glDeleteTextures(1, &mSSAONoiseTexture);
	}

	glGenTextures(1, &mSSAONoiseTexture);
	glBindTexture(GL_TEXTURE_2D, mSSAONoiseTexture);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, 4, 4, 0, GL_RGB, GL_FLOAT, &noise_vectors[0][0]);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
}

