#include "fonts.h"

#include <stb/stb_image_write.h>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <glm/gtc/type_ptr.hpp>

#include "shader.h"
#include "engine.h"
#include "constants.h"
#include "internal.h"
#include "unicode.h"

namespace kek {

Shader *fontShader = nullptr;

TextObject::TextObject(Font *font, std::string text) {
	assert(text.length() > 0);

	this->font = font;
	this->text = text;
	loadChars();
}

TextObject::~TextObject() {
	destroy();
}

void TextObject::destroy() {
	for(auto o : blocks) {
		glDeleteVertexArrays(1, &o.second.vao);
		glDeleteBuffers(1, &o.second.vbo);
	}

	blocks.clear();
}

TextObject::TextObject(TextObject &&other): blocks(other.blocks) {
	other.blocks.clear();
}

TextObject &TextObject::operator=(TextObject &&other) {
	if(this != &other) {
		destroy();

		std::swap(blocks, other.blocks);
	}

	return *this;
}

Font *TextObject::getFont() {
	return font;
}

void TextObject::setText(std::string text) {
	this->text = text;
	loadChars();
}

std::string TextObject::getText() {
	return text;
}

TextMetrics TextObject::getMetrics(int sizePixels) {
	float sizeRatio = (float) sizePixels / KEK_FONT_RESOLUTION;
	return TextMetrics((int) (sizeRatio * offsetX), (int) (sizeRatio * offsetY), (int) (sizeRatio * width), (int) (sizeRatio * height));
}

struct RenderChar {
	float data[24];

	RenderChar(float xPos, float yPos, float w, float h, float texL, float texR, float texU, float texD) {
		float coords[] = {
			xPos,		yPos,		texL, texU,
			xPos,		yPos + h,	texL, texD,
			xPos + w, 	yPos,		texR, texU,
			xPos + w,	yPos,		texR, texU,
			xPos,		yPos + h,	texL, texD,
			xPos + w, 	yPos + h,	texR, texD
		};
		for(int i = 0; i < 24; i++) data[i] = coords[i];
	}
};

void TextObject::allocateBuffer(TextBlock *block, int numChars) {
	unsigned int targetSize = (numChars + KEK_TEXT_BLOCK_SIZE - 1) / KEK_TEXT_BLOCK_SIZE * KEK_TEXT_BLOCK_SIZE; // ceil(numChars / KEK_TEXT_BLOCK_SIZE) * KEK_TEXT_BLOCK_SIZE
	block->bufferSize = targetSize;

	glGenVertexArrays(1, &block->vao);
	glGenBuffers(1, &block->vbo);
	glBindVertexArray(block->vao);

	glBindBuffer(GL_ARRAY_BUFFER, block->vbo);
	glBufferData(GL_ARRAY_BUFFER, targetSize * sizeof(RenderChar), nullptr, GL_DYNAMIC_DRAW); // 6 verts/char, 4 floats/vertex

	glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0);
	glEnableVertexAttribArray(0);

	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *) (2 * sizeof(float)));
	glEnableVertexAttribArray(1);

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

void TextObject::loadChars() {
	std::map<unsigned int, std::vector<RenderChar>> chars;
	std::u32string str = Unicode::convertStdToU32(text);

	float x = 0, y = 0;
	int lineHeight = font->getDefaultMetrics().lineHeight;
	int offX = -1, offY = 0, maxH = 0;
	for(char32_t codepoint : str) {
		if(codepoint == U'\n') {
			y += lineHeight;
			x = 0;
			continue;
		}

		unsigned int charBlock = codepoint >> KEK_FONT_BITMAP_CHAR_BITS;
		CharacterBlock block = font->getCharacterBlock(charBlock);
		Character ch = block.characters[codepoint & KEK_FONT_BITMAP_CHAR_MASK];

		float xPos = x + ch.bearing.x;
		float yPos = y - ch.bearing.y;
		if(offX == -1) offX = ch.bearing.x;
		if(ch.bearing.y > offY) offY = ch.bearing.y;
		float w = ch.size.x;
		float h = ch.size.y;
		if(h > maxH) maxH = h;

		float texL = (ch.textureX) / (float) KEK_FONT_BITMAP_WIDTH;
		float texR = (ch.textureX + ch.size.x) / (float) KEK_FONT_BITMAP_WIDTH;
		float texU = (ch.textureY) / (float) KEK_FONT_BITMAP_HEIGHT;
		float texD = (ch.textureY + ch.size.y) / (float) KEK_FONT_BITMAP_HEIGHT;

		RenderChar rCh = RenderChar(xPos, yPos, w, h, texL, texR, texU, texD);

		auto charsForBlock = chars.find(charBlock);
		if(charsForBlock != chars.end()) {
			charsForBlock->second.push_back(rCh);
		}else {
			std::vector<RenderChar> chs;
			chs.push_back(rCh);
			chars[charBlock] = chs;
		}

		x += (ch.advance >> 6); // == ch.advance / 64
	}

	this->offsetX = offX;
	this->offsetY = offY;
	this->width = x;
	this->height = maxH;

	auto it = chars.begin();
	while(it != chars.end()) {
		auto oldTextBlock = blocks.find(it->first);
		int textLength = it->second.size();

		if(oldTextBlock != blocks.end()) {
			// Update buffer

			TextBlock *oldBlock = &oldTextBlock->second;
			if(textLength != 0 && (textLength > (int) oldBlock->bufferSize || textLength < (int) oldBlock->bufferSize - 2 * KEK_TEXT_BLOCK_SIZE)) {
				// String is either too large for the current buffer or at least one block smaller, reallocate
				// TODO: no need to delete, just call glBufferData
				glDeleteVertexArrays(1, &oldBlock->vao);
				glDeleteBuffers(1, &oldBlock->vbo);
				allocateBuffer(oldBlock, text.length());
			}

			oldBlock->length = textLength;
			glNamedBufferSubData(oldBlock->vbo, 0, textLength * sizeof(RenderChar), &it->second[0]);
		}else {
			// Allocate Buffer
			TextBlock block;
			allocateBuffer(&block, textLength);
			block.length = textLength;
			block.characterBlock = it->first;
			glNamedBufferSubData(block.vbo, 0, textLength * sizeof(RenderChar), &it->second[0]);
			blocks[it->first] = block;
		}

		it++;
	}

	auto it2 = blocks.begin();
	while(it2 != blocks.end()) {
		if(chars.find(it2->first) == chars.end()) {
			it2 = blocks.erase(it2);
		}else {
			it2++;
		}
	}
}

Font::Font(std::string path) {
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

	fileMemory = Resource::loadResource(path);

	if(!fileMemory) {
		std::cerr << "Failed to load font: " << path << std::endl;
		throw std::exception();
	}

	if(FT_Error err = FT_New_Memory_Face(kekData.freetype, (FT_Byte *) fileMemory->buffer, fileMemory->length, 0, &face)) {
		std::cout << "Failed to load font: " << FT_Error_String(err) << std::endl;
		return;
	}

	FT_Set_Pixel_Sizes(face, 0, KEK_FONT_RESOLUTION);
	FT_Select_Charmap(face, FT_Encoding::FT_ENCODING_UNICODE);

	lineHeight = face->size->metrics.height;
	ascender = face->size->metrics.ascender;
	descender = face->size->metrics.descender;

	if(!fontShader) fontShader = new Shader("shader/text/vertex.glsl", "shader/text/fragment.glsl");
}

Font::~Font() {
	FT_Done_Face(face);
	delete fileMemory;
}

CharacterBlock Font::getCharacterBlock(unsigned int block) {
	auto b = characterBlocks.find(block);
	if(b == characterBlocks.end()) return generateCharacterBlock(block);
	return b->second;
}

CharacterBlock Font::generateCharacterBlock(unsigned int block) {
	CharacterBlock chBlock;
	unsigned char *textureBytes = new unsigned char[KEK_FONT_BITMAP_WIDTH * KEK_FONT_BITMAP_HEIGHT]; // grayscale (1 byte per pixel) texture
	memset(textureBytes, 0, KEK_FONT_BITMAP_WIDTH * KEK_FONT_BITMAP_HEIGHT);

	for(unsigned int c = 0; c < KEK_FONT_BITMAP_WIDTH_BLOCKS * KEK_FONT_BITMAP_HEIGHT_BLOCKS; c++) {
		if(FT_Error err = FT_Load_Char(face, (block << KEK_FONT_BITMAP_CHAR_BITS) + c, FT_LOAD_RENDER)) {
			std::cout << "Failed to load glyph: " << FT_Error_String(err) << std::endl;
			continue;
		}

		int col = c % KEK_FONT_BITMAP_WIDTH_BLOCKS;
		int row = c / KEK_FONT_BITMAP_WIDTH_BLOCKS;

		for(unsigned int r = 0; r < face->glyph->bitmap.rows; r++) {
			memcpy(
				textureBytes + row * KEK_FONT_RESOLUTION * KEK_FONT_BITMAP_WIDTH + col * KEK_FONT_RESOLUTION + r * KEK_FONT_BITMAP_WIDTH,
				face->glyph->bitmap.buffer + r * face->glyph->bitmap.width,
				face->glyph->bitmap.width);
		}

		Character ch;
		ch.textureX = col * KEK_FONT_RESOLUTION;
		ch.textureY = row * KEK_FONT_RESOLUTION;
		ch.size = glm::ivec2(face->glyph->bitmap.width, face->glyph->bitmap.rows);
		ch.bearing = glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top);
		ch.advance = face->glyph->advance.x;
		chBlock.characters[c] = ch;
	}

	// stbi_write_png(("/home/mr/Desktop/bitmap_" + std::to_string(block) + ".png").c_str(), KEK_FONT_BITMAP_WIDTH, KEK_FONT_BITMAP_HEIGHT, 1, textureBytes, KEK_FONT_BITMAP_WIDTH);

	glGenTextures(1, &chBlock.textureID);
	glBindTexture(GL_TEXTURE_2D, chBlock.textureID);
	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_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RED, KEK_FONT_BITMAP_WIDTH, KEK_FONT_BITMAP_HEIGHT, 0, GL_RED, GL_UNSIGNED_BYTE, textureBytes);
	glGenerateMipmap(GL_TEXTURE_2D);

	delete[] textureBytes;

	characterBlocks[block] = chBlock;

	return chBlock;
}

TextObject *Font::createText(std::string text) {
	return new TextObject(this, text);
}

FontMetrics Font::getDefaultMetrics() {
	return FontMetrics(lineHeight >> 6, ascender >> 6, descender >> 6);
}

FontMetrics Font::getMetrics(int sizePixels) {
	float sizeRatio = (float) sizePixels / KEK_FONT_RESOLUTION;
	return FontMetrics(((int) (sizeRatio * lineHeight)) >> 6, ((int) (sizeRatio * ascender)) >> 6, ((int) (sizeRatio * descender)) >> 6);
}

void Font::drawText(TextObject *textObject, int x, int y, int sizePixels, Color color) {
	glm::mat4 projection = glm::ortho(0.0f, (float) kekData.screenWidth, (float) kekData.screenHeight, 0.0f);
	Font::drawText(textObject, projection, x, y, sizePixels, color);
}

void Font::drawText(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color) {
	fontShader->use();

	for(auto b : textObject->blocks) {
		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, getCharacterBlock(b.second.characterBlock).textureID);
		glUniform4fv(glGetUniformLocation(fontShader->id, "textColor"), 1, color.valuePointer());
		glUniform1i(glGetUniformLocation(fontShader->id, "charTexture"), 0);
		glBindVertexArray(b.second.vao);

		glUniformMatrix4fv(glGetUniformLocation(fontShader->id, "projection"), 1, GL_FALSE, glm::value_ptr(projection));
		glUniform2fv(glGetUniformLocation(fontShader->id, "offset"), 1, glm::value_ptr(glm::vec2(x, y)));
		glUniform1f(glGetUniformLocation(fontShader->id, "scale"), (float) sizePixels / KEK_FONT_RESOLUTION);

		glDrawArrays(GL_TRIANGLES, 0, 6 * b.second.length);
	}
}

void Font::drawTextFromOrigin(TextObject *textObject, int x, int y, int sizePixels, Color color) {
	TextMetrics metrics = textObject->getMetrics(sizePixels);
	Font::drawText(textObject, x - metrics.offsetX, y + metrics.offsetY, sizePixels, color);
}

void Font::drawTextFromOrigin(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color) {
	TextMetrics metrics = textObject->getMetrics(sizePixels);
	Font::drawText(textObject, projection, x - metrics.offsetX, y + metrics.offsetY, sizePixels, color);
}

void Font::drawTextCentered(TextObject *textObject, int x, int y, int sizePixels, Color color) {
	TextMetrics metrics = textObject->getMetrics(sizePixels);
	Font::drawText(textObject, x - metrics.offsetX + metrics.width / 2, y + metrics.offsetY - metrics.height / 2, sizePixels, color);
}

void Font::drawTextCentered(TextObject *textObject, glm::mat4 projection, int x, int y, int sizePixels, Color color) {
	TextMetrics metrics = textObject->getMetrics(sizePixels);
	Font::drawText(textObject, projection, x - metrics.offsetX + metrics.width / 2, y + metrics.offsetY - metrics.height / 2, sizePixels, color);
}

}
