#include <string.h> // strlen
#include <locale.h>

#include <iostream>
#include <fstream>

#include "RenderModule.h"
#include "RenderUtils.h"
#include "Globals.h"
#include "FileModificationObserver.h"

#include "imgui/imgui.h"

using namespace SimpleMath::GL;

typedef tinygltf::TinyGLTF GLTFLoader;

static GLTFLoader gLoader;

//
// Globals
//
static VertexArray sVertexArray;
static VertexArrayMesh sCoordinateFrameMesh;
static VertexArrayMesh sUnitCubeMesh;
static VertexArrayMesh sUnitCubeLines;
static VertexArrayMesh sIcoSphere;
static VertexArrayMesh sBoneBody;

//
// Camera
//
void Camera::UpdateMatrices() {
	mViewMatrix = LookAt(mEye, mPoi, mUp);

	if (mIsOrthographic) {
		float width = mWidth * 0.5f * (mFar - mNear * 0.5f) * 0.001f;
		float height = width * mHeight / mWidth;

		mProjectionMatrix = Ortho(-width * 0.5f, width * 0.5f, -height * 0.5f, height * 0.5f, mNear, mFar);
	} else {
		mProjectionMatrix = Perspective(mFov, mWidth / mHeight, mNear, mFar);
	}
}

void Camera::DrawGui() {
	ImGui::Text("Width %3.4f, Height %3.4f", mWidth, mHeight);

	ImGui::SliderFloat3("Eye", mEye.data(), -10.0f, 10.0f);
	ImGui::SliderFloat3("Poi", mPoi.data(), -10.0f, 10.0f);
	ImGui::InputFloat3("Up", mUp.data(), -10.0f, 10.0f);
	ImGui::Checkbox("Orthographic", &mIsOrthographic);
	ImGui::SliderFloat("Fov", &mFov, 5, 160);
	ImGui::SliderFloat("Near", &mNear, 0.001, 10);
	ImGui::SliderFloat("Far", &mFar, -20, 50);
	if (ImGui::Button("Reset")) {
		*this = Camera();
	}
}


//
// RenderProgram
//

RenderProgram::~RenderProgram() {
	if (mProgramId != -1)
		glDeleteProgram(mProgramId);
}


GLuint RenderProgram::CompileVertexShader() {
	// Create the shaders
	GLuint VertexShaderID = glCreateShader(GL_VERTEX_SHADER);

	// Read the Vertex Shader code from the file
	std::string VertexShaderCode;
	std::ifstream VertexShaderStream(mVertexShaderFilename.c_str(), std::ios::in);
	if(VertexShaderStream.is_open()){
		std::stringstream sstr;
		sstr << VertexShaderStream.rdbuf();
		VertexShaderCode = sstr.str();
		VertexShaderStream.close();
	}else{
		gLog("Impossible to open %s. Are you in the right directory ? Don't forget to read the FAQ !", mVertexShaderFilename.c_str());
		getchar();
		return -1;
	}

	GLint Result = GL_FALSE;
	int InfoLogLength;

	// Compile Vertex Shader
	gLog("Compiling shader : %s", mVertexShaderFilename.c_str());
	char const * VertexSourcePointer = VertexShaderCode.c_str();
	glShaderSource(VertexShaderID, 1, &VertexSourcePointer , NULL);
	glCompileShader(VertexShaderID);

	// Check Vertex Shader
	glGetShaderiv(VertexShaderID, GL_COMPILE_STATUS, &Result);
	glGetShaderiv(VertexShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
	if ( InfoLogLength > 0 ){
		std::vector<char> VertexShaderErrorMessage(InfoLogLength+1);
		glGetShaderInfoLog(VertexShaderID, InfoLogLength, NULL, &VertexShaderErrorMessage[0]);
		gLog("%s", &VertexShaderErrorMessage[0]);

		return -1;
	}

	return VertexShaderID;
}

GLuint RenderProgram::CompileFragmentShader() {
	GLuint FragmentShaderID = glCreateShader(GL_FRAGMENT_SHADER);

	// Read the Fragment Shader code from the file
	std::string FragmentShaderCode;
	std::ifstream FragmentShaderStream(mFragmentShaderFilename.c_str(), std::ios::in);
	if(FragmentShaderStream.is_open()){
		std::stringstream sstr;
		sstr << FragmentShaderStream.rdbuf();
		FragmentShaderCode = sstr.str();
		FragmentShaderStream.close();
	}
	
	GLint Result = GL_FALSE;
	int InfoLogLength;

	// Compile Fragment Shader
	gLog("Compiling shader : %s", mFragmentShaderFilename.c_str());
	char const * FragmentSourcePointer = FragmentShaderCode.c_str();
	glShaderSource(FragmentShaderID, 1, &FragmentSourcePointer , NULL);
	glCompileShader(FragmentShaderID);

	// Check Fragment Shader
	glGetShaderiv(FragmentShaderID, GL_COMPILE_STATUS, &Result);
	glGetShaderiv(FragmentShaderID, GL_INFO_LOG_LENGTH, &InfoLogLength);
	if ( InfoLogLength > 0 ){
		std::vector<char> FragmentShaderErrorMessage(InfoLogLength+1);
		glGetShaderInfoLog(FragmentShaderID, InfoLogLength, NULL, &FragmentShaderErrorMessage[0]);
		gLog("%s", &FragmentShaderErrorMessage[0]);
		
		return -1;
	}

	return FragmentShaderID;
}

GLuint RenderProgram::LinkProgram(GLuint vertex_shader, GLuint fragment_shader) {
	// Link the program
	gLog("Linking program");
	GLuint ProgramID = glCreateProgram();
	glAttachShader(ProgramID, vertex_shader);
	glAttachShader(ProgramID, fragment_shader);

	// Bind attribute locations
	glBindAttribLocation(ProgramID, 0, "inCoord");
	glBindAttribLocation(ProgramID, 1, "inNormal");
	glBindAttribLocation(ProgramID, 2, "inUV");
	glBindAttribLocation(ProgramID, 3, "inColor");

	glBindFragDataLocation(ProgramID, 0, "outColor");
	glBindFragDataLocation(ProgramID, 1, "outNormal");
	glBindFragDataLocation(ProgramID, 2, "outPosition");

	glLinkProgram(ProgramID);

	GLint Result = GL_FALSE;
	int InfoLogLength;

	// Check the program
	glGetProgramiv(ProgramID, GL_LINK_STATUS, &Result);
	glGetProgramiv(ProgramID, GL_INFO_LOG_LENGTH, &InfoLogLength);
	if ( InfoLogLength > 0 ){
		std::vector<char> ProgramErrorMessage(InfoLogLength+1);
		glGetProgramInfoLog(ProgramID, InfoLogLength, NULL, &ProgramErrorMessage[0]);
		gLog("%s", &ProgramErrorMessage[0]);
	}

	return ProgramID;
}

bool RenderProgram::Load() {
	GLuint vertex_shader_id = CompileVertexShader();
	if (vertex_shader_id == -1) {
		return false;
	}

	GLuint fragment_shader_id = CompileFragmentShader();
	if (fragment_shader_id == -1) {
		glDeleteShader(vertex_shader_id);
		return false;
	}

	mProgramId = LinkProgram(vertex_shader_id, fragment_shader_id);
	if (mProgramId == -1) {
		glDeleteShader(vertex_shader_id);
		glDeleteShader(fragment_shader_id);

		return false;
	}

	glDetachShader(mProgramId, vertex_shader_id);
	glDetachShader(mProgramId, fragment_shader_id);

	glDeleteShader(vertex_shader_id);
	glDeleteShader(fragment_shader_id);

	return true;
}

GLuint RenderProgram::GetUniformLocation(const std::string& name) {
	if (mProgramId == -1) {
		gLog("Cannot get uniform '%s' for program '%s' and '%s': shader not valid.", 
				name.c_str(),
				mVertexShaderFilename.c_str(),
				mFragmentShaderFilename.c_str()
				);
		assert(mProgramId != -1);
	}
	GLuint result = glGetUniformLocation(mProgramId, name.c_str());
	if (result == -1) {
		gLog ("Error loading uniform '%s' from shaders '%s' and '%s': uniform not found.",
				name.c_str(),
				mVertexShaderFilename.c_str(), 
				mFragmentShaderFilename.c_str()
				);
		assert(false);
	} else {
		gLog ("Uniform '%s': %d", name.c_str(), result);
	}

	return result;
}

void RenderProgram::RegisterFileModification() {
	gFileModificationObserver->AddListener(mVertexShaderFilename, this);	
	gFileModificationObserver->AddListener(mFragmentShaderFilename, this);	
}

bool RenderProgram::OnFileChanged(const std::string& filename) {
	gLog("Renderprogram reload as file %s changed", filename.c_str());

	GLuint vertex_shader_id = CompileVertexShader();
	if (vertex_shader_id == -1) {
		gLog ("Reload failed: error when compiling vertex shader");
		return true;
	}

	GLuint fragment_shader_id = CompileFragmentShader();
	if (fragment_shader_id == -1) {
		glDeleteShader(vertex_shader_id);
		gLog ("Reload failed: error when compiling fragment shader");
		return false;
	}

	mProgramId = LinkProgram(vertex_shader_id, fragment_shader_id);
	if (mProgramId == -1) {
		glDeleteShader(vertex_shader_id);
		glDeleteShader(fragment_shader_id);

		gLog ("Reload failed: error when linking the program");

		return false;
	}

	glDetachShader(mProgramId, vertex_shader_id);
	glDetachShader(mProgramId, fragment_shader_id);

	glDeleteShader(vertex_shader_id);
	glDeleteShader(fragment_shader_id);

	gLog ("Reload successful");

	return true;
}

//
// RenderTarget
//
RenderTarget::~RenderTarget() {
	Cleanup();
}

void RenderTarget::Initialize(int width, int height, int flags) {
	Cleanup();

	mFlags = flags;

	Resize(width, height, mFlags);
}

void RenderTarget::Bind() {
	assert(glIsFramebuffer(mFrameBufferId));
	glBindFramebuffer(GL_FRAMEBUFFER, mFrameBufferId);

	GLenum buffers[8];
	int num_buffers = 0;

	if (mFlags & EnableColor) {
		buffers[num_buffers++] = GL_COLOR_ATTACHMENT0;
	}

	if (mFlags & EnableNormalTexture) {
		buffers[num_buffers++] = GL_COLOR_ATTACHMENT1;
	}

	if (mFlags & EnablePositionTexture ) {
		buffers[num_buffers++] = GL_COLOR_ATTACHMENT2;
	}

	if (mFlags & EnableNormalTexture) {
		buffers[num_buffers++] = GL_COLOR_ATTACHMENT3;
	}

	glDrawBuffers(num_buffers, buffers);
}

void RenderTarget::Cleanup() {
	if (mFrameBufferId != -1) {
		glDeleteFramebuffers(1, &mFrameBufferId);
		mFrameBufferId = -1;
	}

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

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

	if (mDepthBuffer != -1) {
		glDeleteRenderbuffers(1, &mDepthBuffer);
		mDepthBuffer = -1;
	}

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

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

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

	mWidth = -1;
	mHeight = -1;
	mFlags = 0;
}

void RenderTarget::Resize(int width, int height, int flags) {
	if (width == mWidth && height == mHeight && flags == mFlags)
		return;

	Cleanup();

	mFlags = flags;

	gLog("Resizing RenderTarget to %d,%d flags: %d", width, height, flags);

	mWidth = width;
	mHeight = height;

	glGenFramebuffers(1, &mFrameBufferId);
	glBindFramebuffer(GL_FRAMEBUFFER, mFrameBufferId);

	if (mFlags & EnableColor) {
		glGenTextures(1, &mColorTexture);
		glBindTexture(GL_TEXTURE_2D, mColorTexture);

		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, mWidth, mHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);

		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);

		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mColorTexture, 0);
	}

	if (mFlags & EnableDepthTexture) {
		assert((mFlags & EnableDepth) == false);

		glGenTextures(1, &mDepthTexture);
		glBindTexture(GL_TEXTURE_2D, mDepthTexture);

		glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, mWidth, mHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0);

		// Set parameters so that we can set a shadow2DSampler
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
		float border_color[] = { 1.0f, 1.0f, 1.0f, 1.0f };
		glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);

		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, mDepthTexture, 0);

		if (mFlags & EnableLinearizedDepthTexture) {
			glGenTextures(1, &mLinearizedDepthTexture);
			glBindTexture(GL_TEXTURE_2D, mLinearizedDepthTexture);

			glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, mWidth, mHeight, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT4, GL_TEXTURE_2D, mLinearizedDepthTexture, 0);
		}
	} else if (mFlags & EnableDepth) {
		assert((mFlags & EnableDepthTexture) == false);
		glGenRenderbuffers(1, &mDepthBuffer);
		glBindRenderbuffer(GL_RENDERBUFFER, mDepthBuffer);
		glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, mWidth, mHeight);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, mDepthBuffer);
	}

	if (mFlags & EnableNormalTexture) {
		glGenTextures(1, &mNormalTexture);
		glBindTexture(GL_TEXTURE_2D, mNormalTexture);

		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, mWidth, mHeight, 0, GL_RGB, GL_FLOAT, NULL);
		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_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, mNormalTexture, 0);
	}

	if (mFlags & EnablePositionTexture) {
		glGenTextures(1, &mPositionTexture);
		glBindTexture(GL_TEXTURE_2D, mPositionTexture);

		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, mWidth, mHeight, 0, GL_RGB, GL_FLOAT, NULL);
		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_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, mPositionTexture, 0);
	}

	GLenum result = glCheckFramebufferStatus(GL_FRAMEBUFFER);
	if (result != GL_FRAMEBUFFER_COMPLETE) {
		switch (result) {
			case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: gLog("Error: GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
																								 break;
			case GL_FRAMEBUFFER_UNDEFINED: gLog("Error: GL_FRAMEBUFFER_UNDEFINED");
																								 break;
			case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: gLog("Error: GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
																												 break;
			default: gLog("Error when creating Framebuffer: %d", result);
		}
	}

	assert(result == GL_FRAMEBUFFER_COMPLETE);
}

void RenderTarget::RenderToLinearizedDepth(const float& near, const float& far, bool is_orthographic) {
	assert(mFlags & EnableLinearizedDepthTexture);
	assert(mLinearizedDepthTexture != -1);
	assert(mVertexArray != nullptr);
	assert(mQuadMesh != nullptr);

	glBindFramebuffer(GL_FRAMEBUFFER, mFrameBufferId);
	GLenum draw_attachment_1[] = { GL_COLOR_ATTACHMENT4 };
	glDrawBuffers(1, draw_attachment_1);

	glClear(GL_COLOR_BUFFER_BIT);
	glDisable(GL_DEPTH_TEST);

	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, mDepthTexture);

	// render depth texture
	glUseProgram(mLinearizeDepthProgram.mProgramId);
	mLinearizeDepthProgram.SetFloat("uNear", near);
	mLinearizeDepthProgram.SetFloat("uFar", far);
	mLinearizeDepthProgram.SetFloat("uIsOrthographic", is_orthographic ? 1.0f : 0.0f);
	mLinearizeDepthProgram.SetInt("uDepthTexture", 0);

	mVertexArray->Bind();
	mQuadMesh->Draw(GL_TRIANGLES);

	if (mFlags & EnableColor) {
		GLenum draw_attachment_0[] = { GL_COLOR_ATTACHMENT0 };
		glDrawBuffers(1, draw_attachment_0);
		glEnable(GL_DEPTH_TEST);
	}
}

//
// Texture
//

Texture::~Texture() {
	if (mTextureId != -1) {
		glDeleteTextures(1, &mTextureId);
		mTextureId = -1;
	}
}

void Texture::MakeGrid(const int& size, const Vector3f &c1, const Vector3f &c2) {
	mWidth = size;
	mHeight = size;

	unsigned char buffer[size * size * 3];
	int size_half = size / 2;

	for (int i = 0; i < size_half; ++i) {
		for (int j = 0; j < size_half; ++j) {
			buffer[(i * size * 3) + (j * 3) + 0] = static_cast<unsigned char>(c1[0] * 255.0f);
			buffer[(i * size * 3) + (j * 3) + 1] = static_cast<unsigned char>(c1[1] * 255.0f);
			buffer[(i * size * 3) + (j * 3) + 2] = static_cast<unsigned char>(c1[2] * 255.0f);
		}
	}

	for (int i = size_half; i < size; ++i) {
		for (int j = 0; j < size_half; ++j) {
			buffer[(i * size * 3) + (j * 3) + 0] = static_cast<unsigned char>(c2[0] * 255.0f);
			buffer[(i * size * 3) + (j * 3) + 1] = static_cast<unsigned char>(c2[1] * 255.0f);
			buffer[(i * size * 3) + (j * 3) + 2] = static_cast<unsigned char>(c2[2] * 255.0f);
		}
	}

	for (int i = size_half; i < size; ++i) {
		for (int j = size_half; j < size; ++j) {
			buffer[(i * size * 3) + (j * 3) + 0] = static_cast<unsigned char>(c1[0] * 255.0f);
			buffer[(i * size * 3) + (j * 3) + 1] = static_cast<unsigned char>(c1[1] * 255.0f);
			buffer[(i * size * 3) + (j * 3) + 2] = static_cast<unsigned char>(c1[2] * 255.0f);
		}
	}

	for (int i = 0; i < size_half; ++i) {
		for (int j = size_half; j < size; ++j) {
			buffer[(i * size * 3) + (j * 3) + 0] = static_cast<unsigned char>(c2[0] * 255.0f);
			buffer[(i * size * 3) + (j * 3) + 1] = static_cast<unsigned char>(c2[1] * 255.0f);
			buffer[(i * size * 3) + (j * 3) + 2] = static_cast<unsigned char>(c2[2] * 255.0f);
		}
	}

	glGenTextures(1, &mTextureId);
	glBindTexture(GL_TEXTURE_2D, mTextureId);

	glTexImage2D(GL_TEXTURE_2D, 
			0,								// level
			GL_RGB,						// internal format
			size,							// width
			size,							// height
			0,								// border (must be 0)
			GL_RGB,						// format of pixel data
			GL_UNSIGNED_BYTE,	// type of pixel data
			buffer						// pixel data
			);

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
}

bool Texture::Load(const char* filename, int num_components) {
//	unsigned char* rgb = stbi_load(filename, &mWidth, &mHeight, num_components);
	assert(false);
	return false;
}

VertexArray::~VertexArray() {
	if (mVertexArrayId != -1) {
		Cleanup();
	}
}

void VertexArray::Initialize(const int& size, GLenum usage) {
	mNumVertices = size;
	mNumUsedVertices = 0;

	glGenVertexArrays (1, &mVertexArrayId);
	glBindVertexArray(mVertexArrayId);

	glGenBuffers(1, &mVertexBuffer);
	glBindBuffer(GL_ARRAY_BUFFER, mVertexBuffer);
	glBufferData(GL_ARRAY_BUFFER, sizeof(VertexData) * mNumVertices, NULL, usage);
}

void VertexArray::Cleanup() {
	gLog ("deleting vertex buffer: %d", mVertexBuffer);
	int is_buffer = glIsBuffer(mVertexBuffer);
	gLog ("is buffer %d", is_buffer);
	glDeleteBuffers(1, &mVertexBuffer);
	mVertexBuffer = -1;
	glDeleteVertexArrays(1, &mVertexArrayId);
	mVertexArrayId = -1;

	mNumVertices = -1;
	mNumUsedVertices = -1;
}

GLuint VertexArray::AllocateMesh(const int& size) {
	if (mNumUsedVertices + size > mNumVertices) {
		gLog("Cannot allocate mesh in VertexArray: not enough vertices available");
		assert(false);
		return -1;
	}

	GLuint offset = mNumUsedVertices;
	mNumUsedVertices += size;
	return offset;
}

void VertexArray::Bind() {
	glBindBuffer(GL_ARRAY_BUFFER, mVertexBuffer);

	// Attribute 0: coords
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(
			VertexAttributePosition,
			4,
			GL_FLOAT,
			GL_FALSE,
			(sizeof(VertexData)),
			(void*)0
			);
	// Attribute 1: normals
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(
			VertexAttributeNormal,
			3,
			GL_FLOAT,
			GL_FALSE,
			(sizeof(VertexData)),
			(void*)(sizeof(float) * 4)
			);
	// Attribute 2: texture coordinates
	glEnableVertexAttribArray(2);
	glVertexAttribPointer(
			VertexAttributeTexCoord0,
			2,
			GL_FLOAT,
			GL_FALSE,
			(sizeof(VertexData)),
			(void*)(sizeof(float) * 7)
			);
	// Attribute 3: color
	glEnableVertexAttribArray(3);
	glVertexAttribPointer(
			VertexAttributeColor,
			4,
			GL_UNSIGNED_BYTE,
			GL_TRUE,
			(sizeof(VertexData)),
			(void*)(sizeof(float) * 9)
			);
}

bool VertexArray::IsBound() {
	GLint bound_vertex_buffer = -1;
	glGetIntegerv (GL_ARRAY_BUFFER_BINDING, &bound_vertex_buffer);
	return bound_vertex_buffer == mVertexBuffer;
}

void VertexArrayMesh::Initialize(VertexArray &array, const int& size) {
	mIndexBuffer = -1;
	mIndexCount = -1;
	
	mVertexArray = &array;
	mIndexOffset = mVertexArray->AllocateMesh(size);
	mOffsetPtr = (void*) (sizeof(VertexArray::VertexData) * mIndexOffset);
}

void VertexArrayMesh::SetData(
		const VertexArray::VertexData* data,
		const int& count
		) {
	// upload the data
	mVertexCount = count;
	glBindBuffer(GL_ARRAY_BUFFER, mVertexArray->mVertexBuffer);

	glBufferSubData(
			GL_ARRAY_BUFFER,
			(GLintptr) mOffsetPtr,
			sizeof(VertexArray::VertexData) * count,
			data
			);

	glBindBuffer(GL_ARRAY_BUFFER, 0);
}

void VertexArrayMesh::SetData(
		const std::vector<Vector4f> &coords,
		const std::vector<Vector3f> &normals,
		const std::vector<Vector2f> &uvs,
		const std::vector<Vector4f> &colors,
		const std::vector<GLuint> &indices
		) {
	assert(mOffsetPtr != (void*) -1);

	int vertex_count = coords.size();
	assert(vertex_count > 0);

	bool have_normals = normals.size() > 0;
	bool have_uvs = uvs.size() > 0;
	bool have_colors = colors.size() > 0;
	
	assert(!have_normals || (normals.size() == vertex_count));
	assert(!have_uvs || (uvs.size() == vertex_count));
	assert(!have_colors || (colors.size() == vertex_count));

	std::vector<VertexArray::VertexData> vertex_data(vertex_count, VertexArray::VertexData());

	for (int i = 0, n = vertex_count; i < n; ++i) {
		memcpy (vertex_data[i].mCoords, coords[i].data(), sizeof(float) * 4);

		if (have_normals) {
			memcpy (vertex_data[i].mNormals, normals[i].data(), sizeof(float) * 3);
		} else {
			memset (vertex_data[i].mNormals, 0, sizeof(float) * 3);
		}

		if (have_uvs) {
			memcpy (vertex_data[i].mTexCoords, uvs[i].data(), sizeof(float) * 2);
		} else {
			memset (vertex_data[i].mTexCoords, 0, sizeof(float) * 2);
		}

		if (have_colors) {
			for (int j = 0; j < 4; ++j) {
				vertex_data[i].mColor[j] = GLubyte(colors[i][j] * 255.0f);
			}
		} else {
			memset (vertex_data[i].mColor, 0, sizeof(float) * 4);
		}
	}

	SetData(
			vertex_data.data(),
			vertex_count
			);
}

void VertexArrayMesh::SetIndexData(const GLuint* indices, const int& count) {
	assert(mIndexBuffer == -1);

	// copy the indices and increase the indices by mIndexOffset
	GLuint temp_buffer[count];
	memcpy (temp_buffer, indices, sizeof(GLuint) * count);
	for (int i = 0; i < count; ++i) {
		temp_buffer[i] += mIndexOffset;
	}

	mIndexCount = count;

	glGenBuffers(1, &mIndexBuffer);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuffer);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(GLuint), temp_buffer, GL_STATIC_DRAW);
}

void VertexArrayMesh::Draw(GLenum mode) {
	this->mVertexArray->Bind();
	assert(mVertexArray->IsBound());

	if (mIndexBuffer == -1) {
		glDrawArrays(mode, mIndexOffset, mVertexCount); 
	} else {
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mIndexBuffer);
		glDrawElements(mode, mIndexCount, GL_UNSIGNED_INT, (void*) 0);
	}

}

//
// AssetFile
//
bool AssetFile::Load(const char* filename) {
	mFilename = filename;
	std::string err;
	bool result = gLoader.LoadASCIIFromFile(&mGLTFModel, &err, mFilename.c_str());
	if (!err.empty()) {
		gLog("Error loading model '%s': %s", mFilename.c_str(), err.c_str());
	} else {
		gLog("Successfully loaded model '%s'", mFilename.c_str());
	}
	
	LoadBuffers();

	return result;
}

void AssetFile::DrawNodeGui(const tinygltf::Node& node) {
	for (int i = 0, n = node.children.size(); i < n; ++i) {
		const int child_node_id = node.children[i];
		const tinygltf::Node& child_node = mGLTFModel.nodes[child_node_id];
		ImGui::PushID("childnode");
		ImGui::PushID("i");
		if (ImGui::TreeNode((void*)(intptr_t)child_node_id, "[%d] %s", child_node_id, child_node.name.c_str())) {
			DrawNodeGui(child_node);
			ImGui::TreePop();
		}
		ImGui::PopID();
		ImGui::PopID();
	}
}

void AssetFile::LoadBuffers() {
	for (int i = 0, n = mGLTFModel.bufferViews.size(); i < n; ++i) {
		const tinygltf::BufferView &buffer_view = mGLTFModel.bufferViews[i];
		if (buffer_view.target == 0) {
			gLog("Warning: buffer_view target at index %d is 0", i);
			continue;
		}

		const tinygltf::Buffer &buffer = mGLTFModel.buffers[buffer_view.buffer];
		GLuint buffer_id;
		glGenBuffers(1, &buffer_id);
		glBindBuffer(buffer_view.target, buffer_id);
		if (buffer_view.name.size() > 0) {
			gLog("Loading Buffer '%s': size %d offset %d", buffer_view.name.c_str(), buffer.data.size(), buffer_view.byteOffset);
		} else {
			gLog("Loading Buffer: size %d offset %d", buffer_view.name.c_str(), buffer.data.size(), buffer_view.byteOffset);
		}
		glBufferData(buffer_view.target, buffer_view.byteLength,
				&buffer.data.at(0) + buffer_view.byteOffset, GL_STATIC_DRAW);
		glBindBuffer(buffer_view.target, 0);
		mBuffers.push_back(buffer_id);
	}
}

VertexAttributeType AssetFile::GetVertexAttributeType(const std::string &attribute_string) const {
	VertexAttributeType attribute_type;
	if (attribute_string.compare("POSITION") == 0) {
		attribute_type = VertexAttributePosition;
	} else if (attribute_string.compare("NORMAL") == 0) {
		attribute_type = VertexAttributeNormal;
	} else if (attribute_string.compare("TEXCOORD_0") == 0) {
		attribute_type = VertexAttributeTexCoord0;
	} else if (attribute_string.compare("JOINTS_0") == 0) {
		attribute_type = VertexAttributeBoneIndex0;
	} else if (attribute_string.compare("WEIGHTS_0") == 0) {
		attribute_type = VertexAttributeBoneWeights0;
	} else {
		attribute_type = VertexAttributeTypeCount;
	}

	return attribute_type;
}

void AssetFile::DrawMesh(const tinygltf::Mesh &mesh, const Matrix44f& matrix) const {
	for (int i = 0, n = mesh.primitives.size(); i < n; ++i) {
		const tinygltf::Primitive& primitive = mesh.primitives[i];

		if (primitive.indices < 0) {
			return;
		}

		std::map<std::string, int>::const_iterator it(primitive.attributes.begin());
		std::map<std::string, int>::const_iterator it_end(primitive.attributes.end());

		for (; it != it_end; it++) {
			assert(it->second >= 0);
			const tinygltf::Accessor& accessor = mGLTFModel.accessors[it->second];
			glBindBuffer(GL_ARRAY_BUFFER, mBuffers[accessor.bufferView]);

			int size = 1;
			switch(accessor.type) {
				case TINYGLTF_TYPE_SCALAR:	size = 1; break;
				case TINYGLTF_TYPE_VEC2: 		size = 2; break;
				case TINYGLTF_TYPE_VEC3: 		size = 3; break;
				case TINYGLTF_TYPE_VEC4: 		size = 4; break;
				default: assert(0); break;
			}

			VertexAttributeType attribute_type = GetVertexAttributeType(it->first);

			if (attribute_type != VertexAttributeTypeCount) {
				int byte_stride = accessor.ByteStride(mGLTFModel.bufferViews[accessor.bufferView]);
				assert(byte_stride != -1);

				glVertexAttribPointer(
						attribute_type,
						size,
						accessor.componentType,
						accessor.normalized ? GL_TRUE : GL_FALSE,
						byte_stride,
						(char *)NULL + accessor.byteOffset);
				glEnableVertexAttribArray(attribute_type);
			}
		}

		const tinygltf::Accessor& index_accessor = mGLTFModel.accessors[primitive.indices];
		glBindBuffer(
				GL_ELEMENT_ARRAY_BUFFER,
				mBuffers[index_accessor.bufferView]
				);

		int mode = -1;
		switch (primitive.mode) {
			case TINYGLTF_MODE_TRIANGLES : mode = GL_TRIANGLES; break;
			case TINYGLTF_MODE_TRIANGLE_STRIP: mode = GL_TRIANGLE_STRIP; break;
			case TINYGLTF_MODE_TRIANGLE_FAN: mode = GL_TRIANGLE_FAN; break;
			case TINYGLTF_MODE_POINTS: mode = GL_POINTS; break;
			case TINYGLTF_MODE_LINE: mode = GL_LINES; break;
			case TINYGLTF_MODE_LINE_LOOP: mode = GL_LINE_LOOP; break;
			default: gLog("Invalid primitive mode: %d", primitive.mode); assert(false); 
		}

		glDrawElements(
				mode,
				index_accessor.count,
				index_accessor.componentType,
				(char *)NULL + index_accessor.byteOffset
				);

		for (; it != it_end; it++) {
			VertexAttributeType attribute_type = GetVertexAttributeType(it->first);
			if (attribute_type != VertexAttributeTypeCount) {
				glDisableVertexAttribArray(attribute_type);
			}
		}
	}
}

void AssetFile::DrawNode(RenderProgram &program, const tinygltf::Node &node, const Matrix44f& matrix) const {
	Matrix44f local_matrix = matrix;
	if (node.matrix.size() == 16) {
		// convert the matrix from double to float
		Matrix44f mat;
		for (int i = 0; i < 16; ++i) {
			mat.data()[i] = node.matrix.data()[i];
		}
		local_matrix *= mat; 
	} else {
		if (node.scale.size() == 3) {
			local_matrix *= ScaleMat44(node.scale[0], node.scale[1], node.scale[2]); 
		}

		if (node.rotation.size() == 4) {
			local_matrix *= Quaternion(node.rotation[0], node.rotation[1], node.rotation[2], node.rotation[3]).toGLMatrix();
		}

		if (node.translation.size() == 3) {
			local_matrix *= TranslateMat44(node.translation[0], node.translation[1], node.translation[2]);
		}
	}

	if (node.mesh >= 0) {
		program.SetMat44("uModelMatrix", local_matrix);
		DrawMesh (mGLTFModel.meshes[node.mesh], local_matrix);
	}

	for (int i = 0; i < node.children.size(); ++i) {
		DrawNode(program, mGLTFModel.nodes[node.children[i]], local_matrix);
	}
}

void AssetFile::DrawModel(RenderProgram& program) const {
	// todo: support non-default scenes
	assert(mGLTFModel.defaultScene >= 0);
	const tinygltf::Scene &scene = mGLTFModel.scenes[mGLTFModel.defaultScene];
	for (int i = 0; i < scene.nodes.size(); ++i) {
		const tinygltf::Node &node = mGLTFModel.nodes[scene.nodes[i]];
		DrawNode(program, node, RotateMat44(90, 0.0, 0.0, 1.0));
	}
}

void AssetFile::DrawGui() {
	ImGui::Text("File: %s", mFilename.c_str());
	if (ImGui::TreeNode("Meshes")) {
		for (int i = 0, n = mGLTFModel.meshes.size(); i < n; ++i) {
			const tinygltf::Mesh& mesh = mGLTFModel.meshes[i];
			ImGui::PushID("mesh");
			if (ImGui::TreeNode((void*)(intptr_t)i, "[%d] %s", i, mesh.name.c_str())) {
				for (int j = 0, m = mesh.primitives.size(); j < m; ++j) {
					const tinygltf::Primitive& primitive
						= mesh.primitives[j];
					if (ImGui::TreeNode("Attributes")) {
						std::map<std::string, int>::const_iterator iter = primitive.attributes.begin();
						while (iter != primitive.attributes.end()) {
							ImGui::Text("%s", iter->first.c_str());
							iter ++;
						}
						ImGui::TreePop();
					}
					ImGui::Text("Indices %d", primitive.indices);
					ImGui::Text("Material %d", primitive.material);
					ImGui::Text("Mode %d", primitive.mode);
				}
				ImGui::TreePop();
			}
			ImGui::PopID();
		}
		ImGui::TreePop();
	}

	if (ImGui::TreeNode("Nodes")) {
		for (int i = 0, n = mGLTFModel.nodes.size(); i < n; ++i) {
			const tinygltf::Node& node = mGLTFModel.nodes[i];
			ImGui::PushID("node");
			if (ImGui::TreeNode((void*)(intptr_t)i, "[%d] %s", i, node.name.c_str())) {
				if (node.camera >= 0) {
					ImGui::Text("Camera %d", node.camera);
				}

				if (node.mesh >= 0) {
					ImGui::Text("Mesh %d", node.mesh);
				}

				if (node.skin >= 0) {
					ImGui::Text("Skin %d", node.skin);
				}

				DrawNodeGui(node);
				ImGui::TreePop();
			}

			ImGui::PopID();	
		}
		ImGui::TreePop();
	}

	if (ImGui::TreeNode("Skins")) {
		for (int i = 0, n = mGLTFModel.skins.size(); i < n; ++i) {
			const tinygltf::Skin& skin = mGLTFModel.skins[i];
			ImGui::PushID("skin");
			if (ImGui::TreeNode((void*)(intptr_t)i, "[%d] %s", i, skin.name.c_str())) {
				ImGui::Text("Skeleton: %d", skin.skeleton);

				if (ImGui::TreeNode("Joints")) {
					for (int j = 0, m = skin.joints.size(); j < m; j++) {
						ImGui::Text("%d", skin.joints[j]);
					}
					ImGui::TreePop();
				}
				ImGui::TreePop();
			}
			ImGui::PopID();
		}
		ImGui::TreePop();
	}
}

//
// Skeleton
//
void Skeleton::UpdateMatrices() {
	int n = mParentIndex.size();
	mMatrices[0] = mLocalTransforms[0].toMatrix();
	for (int i = 1; i < n; i++) {
		int parent_index = mParentIndex[i];
		mMatrices[i] = mMatrices[parent_index] * mLocalTransforms[i].toMatrix();
	}
}

//
// Debug Draw Stuff
//
static void set_icosphere_point(
		VertexArray::VertexData* data,
		unsigned int index,
		const Vector3f &v) {
	assert (index >= 0 && index < 12);

	data[index].x = v[0];
	data[index].y = v[1];
	data[index].z = v[2];
	data[index].w = 1.0f;

	Vector3f vn = v.normalized();
	data[index].nx = vn[0];
	data[index].ny = vn[1];
	data[index].nz = vn[2];

	data[index].s = 0.0f;
	data[index].t = 0.0f;

	data[index].r = 255.0f;
	data[index].g = 255.0f;
	data[index].b = 255.0f;
	data[index].a = 255.0f;
}


void DebugDrawInitialize() {
	// The global VertexArray used for all RenderUtils primitives
	sVertexArray.Initialize(1000, GL_STATIC_DRAW);

	// CoordinateFrame
	sCoordinateFrameMesh.Initialize(sVertexArray, 6);

	VertexArray::VertexData vertex_data[] = {
		{0.0f, 0.0f, 0.0f, 1.0f,	 0.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 0, 0, 255  },
		{1.0f, 0.0f, 0.0f, 1.0f,	 0.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 0, 0, 255  },

		{0.0f, 0.0f, 0.0f, 1.0f,	 0.0f, 0.0f, 0.0f,		0.0f, 0.0f,	  0, 255, 0, 255},
		{0.0f, 1.0f, 0.0f, 1.0f,	 0.0f, 0.0f, 0.0f,		0.0f, 0.0f,	  0, 255, 0, 255},

		{0.0f, 0.0f, 0.0f, 1.0f,	 0.0f, 0.0f, 0.0f,		0.0f, 0.0f,	  0, 0, 255, 255},
		{0.0f, 0.0f, 1.0f, 1.0f,	 0.0f, 0.0f, 0.0f,		0.0f, 0.0f,	  0, 0, 255, 255}
	};

	sCoordinateFrameMesh.SetData(vertex_data, 6);	
	GLuint coordinate_frame_index_data[] = {
		0, 1,
		1, 2,
		3, 4
	};
	sCoordinateFrameMesh.SetIndexData(coordinate_frame_index_data, 6);

	// UnitCubeMesh
	sUnitCubeMesh.Initialize(sVertexArray, 4 * 6);
	VertexArray::VertexData unit_cube_data[] = {
		// front: +x
		{1.0f, 1.0f, 1.0f, 1.0f,		1.0f, 0.0f, 0.0f,		  0.0f, 0.0f,	255, 0, 0, 255 },
		{1.0f, -1.0f, 1.0f, 1.0f,		1.0f, 0.0f, 0.0f,		  0.0f, 0.0f,	255, 0, 0, 255 },
		{1.0f, -1.0f, -1.0f, 1.0f,	1.0f, 0.0f, 0.0f,		  0.0f, 0.0f,	255, 0, 0, 255 },
		{1.0f, 1.0f, -1.0f, 1.0f,		1.0f, 0.0f, 0.0f,		  0.0f, 0.0f,	255, 0, 0, 255 },

		// back: -x
		{-1.0f, 1.0f, 1.0f, 1.0f,		-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 0, 0, 255 },
		{-1.0f, -1.0f, 1.0f, 1.0f,	-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 0, 0, 255 },
		{-1.0f, -1.0f, -1.0f, 1.0f,	-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 0, 0, 255 },
		{-1.0f, 1.0f, -1.0f, 1.0f,	-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 0, 0, 255 },

		// side: +z
		{-1.0f, 1.0f, 1.0f, 1.0f,		0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 0, 0, 255, 255 },	
		{-1.0f, -1.0f, 1.0f, 1.0f,	0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 0, 0, 255, 255 },	
		{1.0f, -1.0f, 1.0f, 1.0f,		0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 0, 0, 255, 255 },	
		{1.0f, 1.0f, 1.0f, 1.0f,		0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 0, 0, 255, 255 },	

		// back side: -z
		{-1.0f, 1.0f, -1.0f, 1.0f,	0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 0, 0, 255, 255 },	
		{-1.0f, -1.0f, -1.0f, 1.0f,	0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 0, 0, 255, 255 },	
		{1.0f, -1.0f, -1.0f, 1.0f,	0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 0, 0, 255, 255 },	
		{1.0f, 1.0f, -1.0f, 1.0f,		0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 0, 0, 255, 255 },	

		// top: +y
		{1.0f, 1.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,		  0.0f, 0.0f, 0, 255, 0, 255 },	
		{1.0f, 1.0f, -1.0f, 1.0f,		0.0f, 1.0f, 0.0f,		  0.0f, 0.0f, 0, 255, 0, 255 },	
		{-1.0f, 1.0f, -1.0f, 1.0f,	0.0f, 1.0f, 0.0f,		  0.0f, 0.0f, 0, 255, 0, 255 },	
		{-1.0f, 1.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,		  0.0f, 0.0f, 0, 255, 0, 255 },	

		// bottom: -y
		{1.0f, -1.0f, 1.0f, 1.0f,		0.0f, -1.0f, 0.0f,		0.0f, 0.0f, 0, 255, 0, 255 },	
		{1.0f, -1.0f, -1.0f, 1.0f,	0.0f, -1.0f, 0.0f,		0.0f, 0.0f, 0, 255, 0, 255 },	
		{-1.0f, -1.0f, -1.0f, 1.0f,	0.0f, -1.0f, 0.0f,		0.0f, 0.0f, 0, 255, 0, 255 },	
		{-1.0f, -1.0f, 1.0f, 1.0f,	0.0f, -1.0f, 0.0f,		0.0f, 0.0f, 0, 255, 0, 255 },	
	};
	sUnitCubeMesh.SetData(unit_cube_data, 4 * 6);
	GLuint unit_cube_index_data[] = {
		0, 1, 2, 2, 3, 0,
		4, 7, 6, 6, 5, 4,
		8, 9, 10, 10, 11, 8,
		12, 15, 14, 14, 13, 12,
		16, 17, 18, 18, 19, 16,
		20, 23, 22, 22, 21, 20
	};
	sUnitCubeMesh.SetIndexData(unit_cube_index_data, 36);

	// Unit Cube (but only lines)
	sUnitCubeLines.Initialize(sVertexArray, 4 * 6);
	VertexArray::VertexData unit_cube_lines_data[] = {
		// front: +x
		{1.0f, 1.0f, 1.0f, 1.0f,		1.0f, 0.0f, 0.0f,		  0.0f, 0.0f,	255, 255, 255, 255 },
		{1.0f, -1.0f, 1.0f, 1.0f,		1.0f, 0.0f, 0.0f,		  0.0f, 0.0f,	255, 255, 255, 255 },
		{1.0f, -1.0f, -1.0f, 1.0f,	1.0f, 0.0f, 0.0f,		  0.0f, 0.0f,	255, 255, 255, 255 },
		{1.0f, 1.0f, -1.0f, 1.0f,		1.0f, 0.0f, 0.0f,		  0.0f, 0.0f,	255, 255, 255, 255 },

		// back: -x
		{-1.0f, 1.0f, 1.0f, 1.0f,		-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
		{-1.0f, -1.0f, 1.0f, 1.0f,	-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
		{-1.0f, -1.0f, -1.0f, 1.0f,	-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
		{-1.0f, 1.0f, -1.0f, 1.0f,	-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },

		// side: +z
		{-1.0f, 1.0f, 1.0f, 1.0f,		0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{-1.0f, -1.0f, 1.0f, 1.0f,	0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{1.0f, -1.0f, 1.0f, 1.0f,		0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{1.0f, 1.0f, 1.0f, 1.0f,		0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	

		// back side: -z
		{-1.0f, 1.0f, -1.0f, 1.0f,	0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{-1.0f, -1.0f, -1.0f, 1.0f,	0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{1.0f, -1.0f, -1.0f, 1.0f,	0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{1.0f, 1.0f, -1.0f, 1.0f,		0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	

		// top: +y
		{1.0f, 1.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{1.0f, 1.0f, -1.0f, 1.0f,		0.0f, 1.0f, 0.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{-1.0f, 1.0f, -1.0f, 1.0f,	0.0f, 1.0f, 0.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{-1.0f, 1.0f, 1.0f, 1.0f,		0.0f, 1.0f, 0.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	

		// bottom: -y
		{1.0f, -1.0f, 1.0f, 1.0f,		0.0f, -1.0f, 0.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{1.0f, -1.0f, -1.0f, 1.0f,	0.0f, -1.0f, 0.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{-1.0f, -1.0f, -1.0f, 1.0f,	0.0f, -1.0f, 0.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{-1.0f, -1.0f, 1.0f, 1.0f,	0.0f, -1.0f, 0.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
	};

	sUnitCubeLines.SetData(unit_cube_lines_data, 4 * 6);

	GLuint unit_cube_lines_index_data[] = {
		0,  1,  1,  2,  2,  3,  3,  0,
		4,  5,  5,  6,  6,  7,  7,  4,
		8,  9,  9, 10, 10, 11, 11,  8,
		12, 13, 13, 14, 14, 15, 15, 12,
		16, 17, 17, 18, 18, 19, 19, 16,
		20, 21, 21, 22, 22, 23, 23, 20,
		12, 14, 13, 15
	};
	sUnitCubeLines.SetIndexData(unit_cube_lines_index_data, 8 * 6 + 4);

	// Icosphere
	VertexArray::VertexData icosphere_data[12];

	float t_icosphere = (1.0f + sqrt(5.0f)) * 0.5f;
	set_icosphere_point(icosphere_data,  0, Vector3f (-1.0f,  t_icosphere, 0.0f));
	set_icosphere_point(icosphere_data,  1, Vector3f ( 1.0f,  t_icosphere, 0.0f));
	set_icosphere_point(icosphere_data,  2, Vector3f (-1.0f, -t_icosphere, 0.0f));
	set_icosphere_point(icosphere_data,  3, Vector3f ( 1.0f, -t_icosphere, 0.0f));

	set_icosphere_point(icosphere_data,  4, Vector3f (0.0f, -1.0f,  t_icosphere));
	set_icosphere_point(icosphere_data,  5, Vector3f (0.0f,  1.0f,  t_icosphere));
	set_icosphere_point(icosphere_data,  6, Vector3f (0.0f, -1.0f, -t_icosphere));
	set_icosphere_point(icosphere_data,  7, Vector3f (0.0f,  1.0f, -t_icosphere));

	set_icosphere_point(icosphere_data,  8, Vector3f ( t_icosphere, 0.0f, -1.0f));
	set_icosphere_point(icosphere_data,  9, Vector3f ( t_icosphere, 0.0f,  1.0f));
	set_icosphere_point(icosphere_data, 10, Vector3f (-t_icosphere, 0.0f, -1.0f));
	set_icosphere_point(icosphere_data, 11, Vector3f (-t_icosphere, 0.0f,  1.0f));

	sIcoSphere.Initialize(sVertexArray, 12);
	sIcoSphere.SetData(icosphere_data, 12);

	GLuint icosphere_index_data[] = {
		0, 11, 5,
		0, 5, 1,
		0, 1, 7,
		0, 7, 10,
		0, 10, 11,

		1, 5, 9,
		5, 11, 4,
		11, 10, 2,
		10, 7, 6,
		7, 1, 8,

		3, 9, 4,
		3, 4, 2,
		3, 2, 6,
		3, 6, 8,
		3, 8, 9,

		4, 9, 5,
		2, 4, 11,
		6, 2, 10,
		8, 6, 7,
		9, 8, 1
	};
	sIcoSphere.SetIndexData(icosphere_index_data, 20 * 3);

	// BoneBody
	sBoneBody.Initialize(sVertexArray, 8 * 3);
	float bone_width = 0.1;
	float bone_bump_fraction = 0.25f;

	VertexArray::VertexData bone_data[] = {
		// lower half
		//
		// front: +x
		{bone_width, bone_bump_fraction, bone_width, 1.0f,				1.0f, 0.0f, 0.0f,			0.0f, 0.0f,	255, 255, 255, 255 },
		{0.0f, 0.0f, 0.0f, 1.0f,																	1.0f, 0.0f, 0.0f,			0.0f, 0.0f,	255, 255, 255, 255 },
		{bone_width, bone_bump_fraction, -bone_width, 1.0f,				1.0f, 0.0f, 0.0f,			0.0f, 0.0f,	255, 255, 255, 255 },
																																										 
		// back: -x																																			
		{-bone_width, bone_bump_fraction, -bone_width, 1.0f,			-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
		{ 0.0f, 0.0f, 0.0f, 1.0f,																	-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
		{-bone_width, bone_bump_fraction, bone_width, 1.0f,				-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
																																										 
		// side: +z                                                                      
		{-bone_width, bone_bump_fraction, bone_width, 1.0f,				0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{0.0f, 0.0f, 0.0f, 1.0f,																	0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{bone_width, bone_bump_fraction, bone_width, 1.0f,				0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
                                                                                     
		// back side: -z                                                                 
		{bone_width, bone_bump_fraction, -bone_width, 1.0f,				0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{0.0f, 0.0f, 0.0f, 1.0f,	  															0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{-bone_width, bone_bump_fraction, -bone_width, 1.0f,							0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },

		// upper half
		//
		// front: +x
		{bone_width, bone_bump_fraction, -bone_width, 1.0f,				1.0f, 0.0f, 0.0f,			0.0f, 0.0f,	255, 255, 255, 255 },
		{0.0f, 1.0f, 0.0f, 1.0f,																	1.0f, 0.0f, 0.0f,			0.0f, 0.0f,	255, 255, 255, 255 },
		{bone_width, bone_bump_fraction, bone_width, 1.0f,				1.0f, 0.0f, 0.0f,			0.0f, 0.0f,	255, 255, 255, 255 },
																																										 
		// back: -x																																			
		{-bone_width, bone_bump_fraction, bone_width, 1.0f,				-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
		{ 0.0f, 1.0f, 0.0f, 1.0f,																	-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
		{-bone_width, bone_bump_fraction, -bone_width, 1.0f,			-1.0f, 0.0f, 0.0f,		0.0f, 0.0f,	255, 255, 255, 255 },
																																										 
		// side: +z																																			
		{bone_width, bone_bump_fraction, bone_width, 1.0f,				0.0f, 0.0f, 1.0f,			0.0f, 0.0f, 255, 255, 255, 255 },	
		{0.0f, 1.0f, 0.0f, 1.0f,																	0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
		{-bone_width, bone_bump_fraction, bone_width, 1.0f,				0.0f, 0.0f, 1.0f,		  0.0f, 0.0f, 255, 255, 255, 255 },	
                                                                                     
		// back side: -z                                                                 
		{-bone_width, bone_bump_fraction, -bone_width, 1.0f,			0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{0.0f, 1.0f, 0.0f, 1.0f,	  															0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	
		{bone_width, bone_bump_fraction, -bone_width, 1.0f,				0.0f, 0.0f, -1.0f,		0.0f, 0.0f, 255, 255, 255, 255 },	

	};
	sBoneBody.SetData(bone_data, 8 * 3);
	GLuint bone_index_data[] = {
		0, 1, 2,
		3, 4, 5,
		6, 7, 8,
		9, 10, 11,
		12, 13, 14,
		15, 16, 17,
		18, 19, 20,
		21, 22, 23
	};
	sBoneBody.SetIndexData(bone_index_data, 24);
}

void DebugDrawCube(RenderProgram &program, const Matrix44f& mat) {
	sVertexArray.Bind();
	program.SetMat44("uModelMatrix", mat);
	program.SetVec4("uColor", Vector4f (1.0f, 1.0f, 1.0f, 1.0f));
	sUnitCubeMesh.Draw(GL_TRIANGLES);
}

void DebugDrawCubeLines(RenderProgram &program, const Matrix44f& mat) {
	sVertexArray.Bind();
	program.SetMat44("uModelMatrix", mat);
	program.SetVec4("uColor", Vector4f (1.0f, 1.0f, 1.0f, 1.0f));
	sUnitCubeLines.Draw(GL_TRIANGLES);
}

void DebugDrawFrame(RenderProgram &program, const Matrix44f& mat) {
	sVertexArray.Bind();
	program.SetMat44("uModelMatrix", mat);
	program.SetVec4("uColor", Vector4f (1.0f, 1.0f, 1.0f, 1.0f));
	sCoordinateFrameMesh.Draw(GL_LINES);
}

void DebugDrawSphere(RenderProgram &program, const Matrix44f& mat, const Vector4f& color) {
	sVertexArray.Bind();
	program.SetMat44("uModelMatrix", mat);
	program.SetVec4("uColor", Vector4f (color[0], color[1], color[2], color[3]));
	sIcoSphere.Draw(GL_TRIANGLES);
}
 
void DebugDrawBone(RenderProgram &program, const Matrix44f& mat, const Vector4f& color) {
	sVertexArray.Bind();

	const float sphere_size = 0.035f;

	DebugDrawSphere(
			program,
			ScaleMat44(sphere_size, sphere_size, sphere_size)
			* mat,
			color
			);

	program.SetMat44("uModelMatrix", mat);
	program.SetVec4("uColor", Vector4f (color[0], color[1], color[2], color[3]));
	sBoneBody.Draw(GL_TRIANGLES);

	DebugDrawSphere(
			program,
			ScaleMat44(sphere_size, sphere_size, sphere_size)
			* mat * TranslateMat44(0.0f, 1.0f, 0.0f),
			color
			);
};
