#include "utility/ftfont.h"
#include "freetype/freetype.h"
#include "utility/logger.h"
#include "utility/data/font_data.h"

#include <glm/gtc/type_ptr.hpp>

Text::Text()
{
	const char* vertexShader = R"(
		#version 330 core
		layout (location = 0) in vec4 vertex;
		layout (std140) uniform Matrices
		{
			mat4 projection;
			mat4 view;
		};
		out vec2 texCoords;
		void main()
		{
			gl_Position = projection * vec4(vertex.x, vertex.y, 0.0, 1.0);
			texCoords = vec2(vertex.z, vertex.w);
		}
	)";
	const char* fragShader = R"(
		#version 330 core
		in vec2 texCoords;
		out vec4 color;
		uniform sampler2D text;
		uniform vec4 textColor;

		void main()
		{
			vec4 sampled = vec4(1.0, 1.0, 1.0, texture(text, texCoords).r);
			color = vec4(textColor) * sampled;
		}
	)";
	unsigned vertexID, fragID;
	int success;
	char log[512];

	vertexID = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertexID, 1, &vertexShader, NULL);
	glCompileShader(vertexID);

	glGetShaderiv(vertexID, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(vertexID, 512, 0, log);
		LOG(ERROR, "FONT VERTEX SHADER FAILED TO COMPILE: {}", log);
		return;
	}

	fragID = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragID, 1, &fragShader, NULL);
	glCompileShader(fragID);

	glGetShaderiv(fragID, GL_COMPILE_STATUS, &success);
	if (!success)
	{
		glGetShaderInfoLog(fragID, 512, NULL, log);
		LOG(ERROR, "FONT FRAGMENT SHADER FAILED TO COMPILE: {}", log);
		return;
	}

	programID = glCreateProgram();
	glAttachShader(programID, vertexID);
	glAttachShader(programID, fragID);
	glLinkProgram(programID);

	glGetProgramiv(programID, GL_LINK_STATUS, &success);
	if (!success)
	{
		glGetProgramInfoLog(programID, 512, NULL, log);
		LOG(ERROR, "ERROR::LINKER FAILED: ", log);
		return;
	}

	colorPos = glGetUniformLocation(programID, "textColor");

	glGenVertexArrays(1, &VAO);
	glGenBuffers(1, &VBO);

	glBindVertexArray(VAO);
	glBindBuffer(GL_ARRAY_BUFFER, VBO);

	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 4, NULL, GL_DYNAMIC_DRAW);
	glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0);
	glEnableVertexAttribArray(0);

	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindVertexArray(0);

	glUniformBlockBinding(programID, glGetUniformBlockIndex(programID, "Matrices"), 0);
}

bool Text::loadFonts(const std::string& font_folder)
{
	FT_Library ft;
	if (FT_Init_FreeType(&ft)) {
		ERROR_LOG("ERROR::FREETYPE Failed to init freetype library", NULL);
	}
	// load every font in the fonts folder then create corresponding textures for each character of each font.
	std::filesystem::path folder(font_folder);
	if (!std::filesystem::exists(folder) || !std::filesystem::is_directory(font_folder))
		ERROR_LOG("{} folder does not exist!", font_folder);

	for (auto& file : std::filesystem::directory_iterator(folder))
	{
		if (!file.path().has_extension() || !file.path().has_filename() || !file.exists() || file.is_directory())
			continue;
		FT_Face face;
		if (FT_New_Face(ft, file.path().string().c_str(), 0, &face))
		{
			LOG(ERROR, "ERROR::FREETYPE Failed to load font: {}", file.path().string());
			continue;
		}
		FT_Set_Pixel_Sizes(face, 0, 48);
		
		glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
		Font f;

		for (int c = 0; c < 128; c++)
		{
			if (FT_Load_Char(face, c, FT_LOAD_RENDER))
			{
				LOG(ERROR, "ERROR::FREETYPE Failed to load glyph ({})", (char)c);
				continue;
			}
			unsigned int texture;
			glGenTextures(1, &texture);
			glBindTexture(GL_TEXTURE_2D, texture);
			glTexImage2D(GL_TEXTURE_2D, 
				0, 
				GL_RED, 
				face->glyph->bitmap.width, 
				face->glyph->bitmap.rows,
				0, 
				GL_RED, 
				GL_UNSIGNED_BYTE, 
				face->glyph->bitmap.buffer
			);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
			Font::Character character = {
				texture,
				glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
				glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
				static_cast<unsigned int>(face->glyph->advance.x)
			};
			f.characters.emplace(std::pair<char, Font::Character>(c, character));
		}
		fonts.emplace(std::pair<std::string, Font>(file.path().filename().string(), f));
		FT_Done_Face(face);
	}
	FT_Done_FreeType(ft);
	return true;
}

void Text::DrawText(
	const std::string& fontName,
	const std::string& text,
	const glm::vec2& position,
	float scale,
	const glm::vec4& color)
{
	auto iterator = fonts.find(fontName);
	if (iterator == fonts.end())
	{
		LOG(ERROR, "Font '{}' not found!", fontName);
		return;
	}

	float x = position.x;
	float y = position.y + iterator->second.characters['A'].Bearing.y * scale;

	glUseProgram(programID);
	//glUniformMatrix4fv(projPos, 1, GL_FALSE, glm::value_ptr(projMatrix));
	glUniform4fv(colorPos, 1, glm::value_ptr(color));
	glActiveTexture(GL_TEXTURE0);
	glBindVertexArray(VAO);

	std::string::const_iterator c;
	for (c = text.begin(); c != text.end(); c++)
	{
		Font::Character ch = iterator->second.characters[*c];

		float xpos = x + ch.Bearing.x * scale;
		float ypos = y - (ch.Bearing.y) * scale;

		float w = ch.Size.x * scale;
		float h = ch.Size.y * scale;

		float vertices[6][4] = {
			{ xpos,		ypos + h,	0.0f, 1.0f },
			{ xpos,		ypos,		0.0f, 0.0f },
			{ xpos + w,	ypos,		1.0f, 0.0f },

			{ xpos,		ypos + h,	0.0f, 1.0f },
			{ xpos + w,	ypos,		1.0f, 0.0f },
			{ xpos + w,	ypos + h,	1.0f, 1.0f }
		};

		glBindTexture(GL_TEXTURE_2D, ch.TextureID);

		glBindBuffer(GL_ARRAY_BUFFER, VBO);
		glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
		glBindBuffer(GL_ARRAY_BUFFER, 0);

		glDrawArrays(GL_TRIANGLES, 0, 6);
		x += (ch.Advance >> 6) * scale;
	}
	glBindVertexArray(0);
	glBindTexture(GL_TEXTURE_2D, 0);
}

// Too lazy to fix rn
void Text::DrawTextOutline(
	const std::string& fontName,
	const std::string& text,
	const glm::vec2& position,
	float scale,
	const glm::vec4& color)
{
	DrawText("comicbd.ttf", text, position, scale, glm::vec4(0.0f, 0.0f, 0.0f, 1.0f));
	DrawText(fontName, text, position, scale, color);
}

DebugText::DebugText() : Text()
{
	loadDebugFont();
}

DebugText* DebugText::instance{ nullptr };
std::mutex DebugText::mutex;

DebugText* DebugText::getInstance()
{
	std::lock_guard<std::mutex> lock(mutex);
	if (instance == nullptr) {
		instance = new DebugText();
	}
	return instance;
}



void DebugText::DrawText(const glm::vec2& pos, 
							  const glm::vec4& color, 
							  const float scale,
							  const std::string& text)
{
		float x = pos.x;
		float y = pos.y + font.characters['A'].Bearing.y * scale;

		glUseProgram(programID);
		glUniform4fv(colorPos, 1, glm::value_ptr(color));
		glActiveTexture(GL_TEXTURE0);
		glBindVertexArray(VAO);

		std::string::const_iterator c;
		for (c = text.begin(); c != text.end(); c++)
		{
			Font::Character ch = font.characters[*c];

			float xpos = x + ch.Bearing.x * scale;
			float ypos = y - (ch.Bearing.y) * scale;

			float w = ch.Size.x * scale;
			float h = ch.Size.y * scale;

			float vertices[6][4] = {
				{ xpos,		ypos + h,	0.0f, 1.0f },
				{ xpos,		ypos,		0.0f, 0.0f },
				{ xpos + w,	ypos,		1.0f, 0.0f },

				{ xpos,		ypos + h,	0.0f, 1.0f },
				{ xpos + w,	ypos,		1.0f, 0.0f },
				{ xpos + w,	ypos + h,	1.0f, 1.0f }
			};

			glBindTexture(GL_TEXTURE_2D, ch.TextureID);

			glBindBuffer(GL_ARRAY_BUFFER, VBO);
			glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
			glBindBuffer(GL_ARRAY_BUFFER, 0);

			glDrawArrays(GL_TRIANGLES, 0, 6);
			x += (ch.Advance >> 6) * scale;
		}
		glBindVertexArray(0);
		glBindTexture(GL_TEXTURE_2D, 0);
}

void DebugText::loadDebugFont()
{
	FT_Library lib;
	FT_Face face;
	if (FT_Init_FreeType(&lib) != FT_Err_Ok) {
		LOG(ERROR, "Failed to init freetype library!", NULL);
		assert(1 == 0); // Force crash
	}
	if (FT_New_Memory_Face(lib, debug_font_data, debug_font_len, 0, &face) != FT_Err_Ok) {
		LOG(ERROR, "Failed to load debug font face from memory!", NULL);
		assert(1 == 0); // Force crash
	}
	FT_Set_Pixel_Sizes(face, 0, 40);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

	for (int c = 0; c < 250; c++) {
		if (FT_Load_Char(face, c, FT_LOAD_RENDER) != FT_Err_Ok) {
			LOG(WARN, "Failed to load char '{}'", (char)c);
			continue;
		}
		unsigned texture;
		glGenTextures(1, &texture);
		glBindTexture(GL_TEXTURE_2D, texture);
		glTexImage2D(GL_TEXTURE_2D, 
							 0, 
							 GL_RED, 
							 face->glyph->bitmap.width,
							 face->glyph->bitmap.rows,
							 0,
							 GL_RED,
							 GL_UNSIGNED_BYTE,
							 face->glyph->bitmap.buffer);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

		Font::Character ch = {
			texture,
			glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows),
			glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top),
			static_cast<unsigned int>(face->glyph->advance.x)
		};
		font.characters.emplace(std::pair<char, Font::Character>((char)c, ch));
	}

	FT_Done_Face(face);
	FT_Done_FreeType(lib);
}

DebugText::~DebugText()
{
	for (auto ch : font.characters) {
		glDeleteBuffers(1, &VBO);
		glDeleteVertexArrays(1, &VAO);
		glDeleteTextures(1, &ch.second.TextureID);
	}
	glDeleteProgram(programID);
}
