/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "TextDrawable.hpp"
#include "Scene/Geometry.hpp"
#include "Scene/Material.hpp"
#include "Scene/Vertex.hpp"
#include "Scene/UniformBuffer.hpp"
#include "Scene/IFontAtlasGenerator.hpp"
#include "Base/Logger.hpp"
#include "Host/ResourceLoader.hpp"
#include "Image/ImageLoader.hpp"
#include "DataFormat.hpp"
#include "Base/Logger.hpp"
#include <fstream>
#include <utf8.h>

namespace OpenVulkano::Scene
{
	Shader& TextDrawable::GetSdfDefaultShader()
	{
		static bool once = true;
		static Shader sdfDefaultShader;
		if (once)
		{
			sdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::VERTEX, "Shader/text");
			sdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::FRAGMENT, "Shader/text");
			sdfDefaultShader.AddVertexInputDescription(OpenVulkano::Vertex::GetVertexInputDescription());
			sdfDefaultShader.AddDescriptorSetLayoutBinding(Texture::DESCRIPTOR_SET_LAYOUT_BINDING);
			DescriptorSetLayoutBinding desc = UniformBuffer::DESCRIPTOR_SET_LAYOUT_BINDING;
			desc.stageFlags = ShaderProgramType::FRAGMENT;
			sdfDefaultShader.AddDescriptorSetLayoutBinding(desc);
			sdfDefaultShader.alphaBlend = true;
			sdfDefaultShader.cullMode = CullMode::NONE;
			once = false;
		}
		return sdfDefaultShader;
	}

	Shader& TextDrawable::GetMsdfDefaultShader()
	{
		static bool once = true;
		static Shader msdfDefaultShader;
		if (once)
		{
			msdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::VERTEX, "Shader/text");
			msdfDefaultShader.AddShaderProgram(OpenVulkano::ShaderProgramType::FRAGMENT, "Shader/msdfText");
			msdfDefaultShader.AddVertexInputDescription(OpenVulkano::Vertex::GetVertexInputDescription());
			msdfDefaultShader.AddDescriptorSetLayoutBinding(Texture::DESCRIPTOR_SET_LAYOUT_BINDING);
			DescriptorSetLayoutBinding desc = UniformBuffer::DESCRIPTOR_SET_LAYOUT_BINDING;
			desc.stageFlags = ShaderProgramType::FRAGMENT;
			msdfDefaultShader.AddDescriptorSetLayoutBinding(desc);
			msdfDefaultShader.alphaBlend = true;
			msdfDefaultShader.cullMode = CullMode::NONE;
			once = false;
		}
		return msdfDefaultShader;
	}

	TextDrawable::TextDrawable(const TextConfig& config)
	{
		m_cfg = config;
		m_uniBuffer.Init(sizeof(TextConfig), &m_cfg, 3);
		m_uniBuffer.binding.stageFlags = ShaderProgramType::FRAGMENT;
	}

	TextDrawable::TextDrawable(const Array<char>& atlasMetadata, const TextConfig& config)
	    : TextDrawable(atlasMetadata, nullptr, config)
	{
	}

	TextDrawable::TextDrawable(const std::string& atlasMetadataFile, const TextConfig& config)
	    : TextDrawable(OpenVulkano::Utils::ReadFile(atlasMetadataFile), nullptr, config)
	{
	}

	TextDrawable::TextDrawable(const std::string& atlasMetadataFile, Texture* atlasTex, const TextConfig& config)
	    : TextDrawable(OpenVulkano::Utils::ReadFile(atlasMetadataFile), atlasTex, config)
	{
	}

	TextDrawable::TextDrawable(const Array<char>& atlasMetadata, Texture* atlasTex, const TextConfig& config)
	{
		uint32_t isPacked;
		std::memcpy(&isPacked, atlasMetadata.Data() + (atlasMetadata.Size() - sizeof(uint32_t)), sizeof(uint32_t));
		uint64_t metadataBytes;
		std::memcpy(&metadataBytes, atlasMetadata.Data() + (atlasMetadata.Size() - sizeof(uint32_t) - sizeof(uint64_t)),
		            sizeof(uint64_t));
		uint64_t offsetToMetadata = atlasMetadata.Size() - metadataBytes - sizeof(uint32_t) - sizeof(uint64_t);
		m_atlasData = std::make_shared<AtlasData>();
		if (isPacked)
		{
			m_material.texture = &m_atlasData->texture;
			m_atlasData->img = Image::IImageLoader::loadData((const uint8_t*) atlasMetadata.Data(),
			                                      offsetToMetadata);
			m_material.texture->format = m_atlasData->img->dataFormat;
			m_material.texture->resolution = m_atlasData->img->resolution;
			m_material.texture->size = m_atlasData->img->data.Size();
			m_material.texture->textureBuffer = m_atlasData->img->data.Data();
		}
		else
		{
			if (atlasTex == nullptr) { throw std::runtime_error("Atlas texture cannot be null with non-packed atlas metadata"); }
			m_atlasData->texture = *atlasTex;
			m_material.texture = atlasTex;
		}

		// metadata info
		size_t read_bytes = offsetToMetadata;
		size_t readMetadataBytes = 0;
		uint32_t unicode = 0;

		std::memcpy(&m_atlasData->meta, atlasMetadata.Data() + read_bytes, sizeof(AtlasMetadata));
		read_bytes += sizeof(AtlasMetadata);
		readMetadataBytes += sizeof(AtlasMetadata);
		while (readMetadataBytes < metadataBytes)
		{
			std::memcpy(&unicode, atlasMetadata.Data() + read_bytes, sizeof(uint32_t));
			read_bytes += sizeof(uint32_t);
			readMetadataBytes += sizeof(uint32_t);
			GlyphInfo& info = m_atlasData->glyphs[unicode];
			std::memcpy(&info, atlasMetadata.Data() + read_bytes, sizeof(GlyphInfo));
			read_bytes += sizeof(GlyphInfo);
			readMetadataBytes += sizeof(GlyphInfo);
		}
		m_cfg = config;
		m_uniBuffer.Init(sizeof(TextConfig), &m_cfg, 3);
		m_uniBuffer.binding.stageFlags = ShaderProgramType::FRAGMENT;
	}

	TextDrawable::TextDrawable(const std::shared_ptr<AtlasData>& atlasData, const TextConfig& config)
	{
		if (!atlasData || atlasData->glyphs.empty() || !atlasData->texture.textureBuffer)
		{
			throw std::runtime_error("Cannot initialize text drawable with empty atlas data");
		}
		m_atlasData = atlasData;
		m_cfg = config;
		m_uniBuffer.Init(sizeof(TextConfig), &m_cfg, 3);
		m_uniBuffer.binding.stageFlags = ShaderProgramType::FRAGMENT;
	}

	void TextDrawable::GenerateText(const std::string& text, const Math::Vector3f& pos)
	{
		if (text.empty())
		{
			return;
		}

		auto GetActualLength = [&]()
		{
			auto begin = text.begin();
			auto end = text.end();
			size_t len = 0;
			while (begin != end)
			{
				uint32_t c = utf8::next(begin, end);
				if (c == '\n') continue;
				++len;
			}
			return len;
		};

		size_t len = GetActualLength();
		m_geometry.Close();
		m_geometry.Init(len * 4, len * 6);
		AtlasMetadata* meta;
		std::map<uint32_t, GlyphInfo>* symbols;
		m_material.texture = &m_atlasData->texture;
		symbols = &m_atlasData->glyphs;
		meta = &m_atlasData->meta;

		double cursorX = pos.x;
		auto begin = text.begin();
		auto end = text.end();
		const double lineHeight = meta->lineHeight;
		double posY = pos.y;
		int i = 0;
		Math::Vector3f bmin(pos), bmax(pos);
		bool firstGlyph = true;
		while (begin != end)
		{
			uint32_t c = utf8::next(begin, end);
			if (c == '\n')
			{
				posY -= lineHeight;
				cursorX = pos.x;
				continue;
			}

			if (symbols->find(c) == symbols->end())
			{
				Logger::RENDER->error("Could not find glyph for character {}", c);
				if (symbols->find(static_cast<uint32_t>('?')) != symbols->end())
				{
					c = static_cast<uint32_t>('?'); 
				}
				else
				{
					Logger::RENDER->error("Could not find glyph for character ? to replace glyph {}", c);
					continue;
				}
			}

			uint32_t vIdx = i * 4;
			uint32_t indices[] = { 1 + vIdx, 2 + vIdx, 3 + vIdx, 1 + vIdx, 3 + vIdx, 0 + vIdx };
			GlyphInfo& info = symbols->at(c);

			// left bottom
			m_geometry.vertices[vIdx].position.x = info.xyz[0].x + cursorX;
			m_geometry.vertices[vIdx].position.y = posY - info.xyz[0].y;
			m_geometry.vertices[vIdx].position.z = info.xyz[0].z;
			m_geometry.vertices[vIdx].textureCoordinates = Math::Vector3f(info.uv[0], 0);

			// right bottom
			m_geometry.vertices[vIdx + 1].position.x = info.xyz[1].x + cursorX;
			m_geometry.vertices[vIdx + 1].position.y = posY - info.xyz[1].y;
			m_geometry.vertices[vIdx + 1].position.z = info.xyz[1].z;
			m_geometry.vertices[vIdx + 1].textureCoordinates = Math::Vector3f(info.uv[1], 0);

			// top right
			m_geometry.vertices[vIdx + 2].position.x = info.xyz[2].x + cursorX;
			m_geometry.vertices[vIdx + 2].position.y = posY + info.xyz[2].y;
			m_geometry.vertices[vIdx + 2].position.z = info.xyz[2].z;
			m_geometry.vertices[vIdx + 2].textureCoordinates = Math::Vector3f(info.uv[2], 0);

			// top left
			m_geometry.vertices[vIdx + 3].position.x = info.xyz[3].x + cursorX;
			m_geometry.vertices[vIdx + 3].position.y = posY + info.xyz[3].y;
			m_geometry.vertices[vIdx + 3].position.z = info.xyz[3].z;
			m_geometry.vertices[vIdx + 3].textureCoordinates = Math::Vector3f(info.uv[3], 0);
			m_geometry.SetIndices(indices, 6, 6 * i);
			// TODO: change to lower value(or ideally remove completely) to avoid overlapping and make less space between symbols
			// when setting for depth comparison operator will be available( <= )
			cursorX += info.advance + 0.08;
			if (firstGlyph)
			{
				bmin.x = m_geometry.vertices[vIdx].position.x;
				firstGlyph = false;
			}
			bmax.x = std::max(bmax.x, m_geometry.vertices[vIdx + 1].position.x);
			bmax.y = std::max(bmax.y, m_geometry.vertices[vIdx + 2].position.y);
			bmin.y = std::min(bmin.y, m_geometry.vertices[vIdx + 1].position.y);
			++i;
		}
		m_bbox.Init(bmin, bmax);
		SimpleDrawable::Init(m_shader, &m_geometry, &m_material, &m_uniBuffer);
	}

	void TextDrawable::SetAtlasData(const std::shared_ptr<AtlasData>& atlasData) 
	{
		if (!atlasData || atlasData->glyphs.empty() || !atlasData->texture.textureBuffer)
		{
			throw std::runtime_error("Cannot initialize text drawable with empty atlas data");
		}
		m_atlasData = atlasData;
	}
}