/*
 * 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 "BitmapFontAtlasGenerator.hpp"
#include "Base/Logger.hpp"
#include "Text/FontAtlas.hpp"
#include <freetype/ftlcdfil.h>

namespace OpenVulkano::Scene
{
	void BitmapFontAtlasGenerator::GenerateAtlas(const std::string& fontFile, const std::set<uint32_t>& charset,
	                                             const std::optional<std::string>& pngOutput)
	{
		Generate(fontFile, charset, pngOutput);
	}

	void BitmapFontAtlasGenerator::GenerateAtlas(const Array<char>& fontData, const std::set<uint32_t>& charset,
	                                             const std::optional<std::string>& pngOutput)
	{
		Generate(fontData, charset, pngOutput);
	}

	void BitmapFontAtlasGenerator::Generate(const std::variant<std::string, Array<char>>& source,
	                                        const std::set<uint32_t>& chset,
	                                        const std::optional<std::string>& pngOutput)
	{
		if (chset.empty())
		{
			Logger::APP->info("Charset is empty. Nothing to generate.");
			return;
		}

		const auto& [lib, face] = FontAtlasGeneratorBase::InitFreetype(source);
		FT_Set_Pixel_Sizes(face.get(), 0, m_pixelSizeConfig.CalculatePixelSize());
		if (m_subpixelLayout != SubpixelLayout::UNKNOWN)
		{
			FT_Error error = FT_Library_SetLcdFilter(lib.get(), FT_LCD_FILTER_DEFAULT);
			if (error != 0)
			{
				m_subpixelLayout = SubpixelLayout::UNKNOWN;
				m_channelsCount = 1;
				Logger::SCENE->error("Failed to set lcd filter for subpixel rendering. {}", GetFreetypeErrorDescription(error));
			}
		}

		auto [allGlyphs, atlasWidth] = InitGlyphsForPacking(chset, face);
		std::vector<Shelf> shelves = Shelf::CreateShelves(atlasWidth, allGlyphs, face, m_channelsCount);
		uint32_t atlasHeight = 0;
		std::for_each(shelves.begin(), shelves.end(), [&](const Shelf& shelf) { atlasHeight += shelf.GetHeight(); });
		const Math::Vector2ui atlasResolution = { atlasWidth, atlasHeight };

		// same as in msdfgen lib by default. see import-font.h for reference
		// TODO: probably also support keeping coordinates as the integer values native to the font file
		// but since some algorithms have already been implemented for EM_NORMALIZED mode, currently there is no support for default font metrics (ints)
		// The coordinates will be normalized to the em size, i.e. 1 = 1 em
		const double scaleFactor = (1. / face->units_per_EM);
		m_atlasData = std::make_shared<FontAtlas>(atlasResolution, face->height * scaleFactor,
		                                          static_cast<bool>(m_subpixelLayout) ? FontAtlasType::BITMAP_SUBPIXEL :
		                                                                                FontAtlasType::BITMAP,
		                                          m_subpixelLayout.GetTextureDataFormat());
		FillGlyphsInfo(allGlyphs, face, scaleFactor);
		if (pngOutput) m_atlasData->Save(*pngOutput);
	}

	std::pair<std::vector<GlyphForPacking>, double> 
		BitmapFontAtlasGenerator::InitGlyphsForPacking(const std::set<uint32_t>& chset, const FtFaceRecPtr& face)
	{
		FT_Error error = 0;
		double area = 0;
		std::vector<GlyphForPacking> allGlyphs;
		allGlyphs.reserve(chset.size());
		for (uint32_t codepoint : chset)
		{
			error = FT_Load_Char(face.get(), codepoint, GetGlyphRenderMode());
			if (error)
			{
				Logger::SCENE->error("FT_Load_Char for codepoint {} has failed. {}", codepoint,
				                     GetFreetypeErrorDescription(error));
				continue;
			}
			// TODO: Try to reduce resulting texture size in subpixel rendering mode,
			// since freetype for some glyphs not only triples width/height by 3, but also adds extra padding and extra(meaningful?) pixels.
			// NOTE: looks like it adds 2 pixels to the left and right in FT_LOAD_TARGET_LCD mode, so we should take this into account in FillSubpixelData.
			// https://freetype.org/freetype2/docs/reference/ft2-lcd_rendering.html
			// So, the possible approach to try is: 
			// 1) render glyph here with FT_LOAD_RENDER mode;
			// 2) render glyph in FillGlyphsInfo with FT_LOAD_RENDER | FT_LOAD_TARGET_LCD mode;
			// 3) take into account all mentioned things above for proper mapping.
			FT_GlyphSlot slot = face->glyph;
			GlyphForPacking& glyph = allGlyphs.emplace_back(codepoint, ScaleGlyphSize(slot->bitmap.width, slot->bitmap.rows));
			area += slot->bitmap.rows * slot->bitmap.width;
		}
		std::sort(allGlyphs.begin(), allGlyphs.end(),
		          [](const GlyphForPacking& a, const GlyphForPacking& b) { return a.size.y > b.size.y; });
		// make atlas in square form, so that atlasWidth +- equals atlasHeight
		return { allGlyphs, ceil(sqrt(area / (m_channelsCount == 1 ? 1 : 3))) };
	}

	Math::Vector2ui BitmapFontAtlasGenerator::ScaleGlyphSize(unsigned int w, unsigned int h) const
	{
		if (m_subpixelLayout == SubpixelLayout::UNKNOWN || m_channelsCount == 1)
		{
			return { w, h };
		}

		if (m_subpixelLayout.IsHorizontalSubpixelLayout())
		{
			assert(w % 3 == 0);
			w /= 3;
		}
		else
		{
			assert(h % 3 == 0);
			h /= 3;
		}
		return { w, h };
	}

	FT_Int32 BitmapFontAtlasGenerator::GetGlyphRenderMode() const
	{
		if (m_channelsCount == 1)
		{
			return FT_LOAD_RENDER;
		}
		FT_Int32 glyphRenderMode = FT_LOAD_RENDER;
		if (m_subpixelLayout < SubpixelLayout::RGBV)
		{
			glyphRenderMode |= FT_LOAD_TARGET_LCD;
		}
		else if (m_subpixelLayout < SubpixelLayout::UNKNOWN)
		{
			glyphRenderMode |= FT_LOAD_TARGET_LCD_V;
		}
		return glyphRenderMode;
	}

	void BitmapFontAtlasGenerator::FillGlyphsInfo(const std::vector<GlyphForPacking>& allGlyphs, const FtFaceRecPtr& face, double scaleFactor)
	{
		size_t loadedGlyphs = 0;
		for (const GlyphForPacking& glyph : allGlyphs)
		{
			FT_Error error = FT_Load_Char(face.get(), glyph.code, GetGlyphRenderMode());
			if (error)
			{
				Logger::SCENE->error("FT_Load_Char for codepoint {} has failed. {}", glyph.code,
				                     GetFreetypeErrorDescription(error));
				continue;
			}

			FT_GlyphSlot slot = face->glyph;
			if (m_channelsCount == 1)
			{
				char* baseAddress = static_cast<char*>(m_atlasData->GetTexture()->textureBuffer)
				                    + glyph.firstGlyphByteInAtlas;
				for (int row = 0; row < slot->bitmap.rows; row++)
				{
					std::memcpy(baseAddress - row * m_atlasData->GetTexture()->resolution.x,
					            &slot->bitmap.buffer[row * slot->bitmap.pitch], slot->bitmap.width);
				}
			}
			else
			{
				FillSubpixelData(slot->bitmap, glyph);
			}

			GlyphInfo& glyphInfo = m_atlasData->GetGlyphs()[glyph.code];
			const Math::Vector2d glyphMetrics = { slot->metrics.width * scaleFactor,
				                                  slot->metrics.height * scaleFactor };
			const Math::Vector2d glyphBearing = { slot->metrics.horiBearingX * scaleFactor,
				                                  slot->metrics.horiBearingY * scaleFactor };

			const Math::Vector2ui scaledAtlasSize = ScaleGlyphSize(slot->bitmap.width, slot->bitmap.rows);
			Math::AABB glyphAtlasAABB(Math::Vector3f(glyph.atlasPos.x, glyph.atlasPos.y, 0), Math::Vector3f(glyph.atlasPos.x + scaledAtlasSize.x, glyph.atlasPos.y + scaledAtlasSize.y, 0));
			SetGlyphData(glyphInfo, glyphBearing, glyphMetrics, glyphAtlasAABB, slot->advance.x * scaleFactor);
			loadedGlyphs++;
		}
		Logger::SCENE->debug("Created atlas with {} glyphs, {} glyphs could not be loaded", loadedGlyphs, allGlyphs.size() - loadedGlyphs);
	}

	void BitmapFontAtlasGenerator::FillSubpixelData(const FT_Bitmap& bitmap, const GlyphForPacking& glyph)
	{
		Texture* tex = m_atlasData->GetTexture();
		char* texBuffer = static_cast<char*>(tex->textureBuffer);
		if (m_subpixelLayout.IsHorizontalSubpixelLayout())
		{
			// RGB RGB RGB
			assert(bitmap.width % 3 == 0);
			for (int row = 0; row < bitmap.rows; row++)
			{
				for (int col = 0, atlasPos = 0; col < bitmap.width; col += 3, atlasPos += 4)
				{
					const size_t bitmapPos = row * bitmap.pitch + col;
					const size_t texturePos = (glyph.firstGlyphByteInAtlas - row * tex->resolution.x * m_channelsCount) + atlasPos;
					const uint8_t rgb[3] = { bitmap.buffer[bitmapPos], bitmap.buffer[bitmapPos + 1],
						                     bitmap.buffer[bitmapPos + 2] };
					std::memcpy(texBuffer + texturePos, rgb, 3);
					texBuffer[texturePos + 3] = 255;
				}
			}
		}
		else
		{
			// RRR
			// GGG
			// BBB
			assert(bitmap.rows % 3 == 0);
			for (int row = 0; row < bitmap.rows; row += 3)
			{
				for (int col = 0; col < bitmap.width; col++)
				{
					const size_t bitmapPos = col + (bitmap.pitch * row);
					const size_t texturePos = (glyph.firstGlyphByteInAtlas + col * m_channelsCount)
					                          - ((row / 3) * (tex->resolution.x * m_channelsCount));
					const uint8_t rgb[3] = { bitmap.buffer[bitmapPos + 2 * bitmap.pitch],
						                     bitmap.buffer[bitmapPos + bitmap.pitch], bitmap.buffer[bitmapPos] };
					std::memcpy(texBuffer + texturePos, rgb, 3);
					texBuffer[texturePos + 3] = 255;
				}
			}
		}
	}
}

