#include "RuntimeTextTextureComposer.h"

#include <algorithm>
#include <cmath>

namespace RenderCadenceCompositor
{
namespace
{
constexpr unsigned kTextTextureHeight = 256;
constexpr unsigned kTextTexturePadding = 16;
constexpr double kFontPixelsPerEm = 192.0;

const FontAtlasBuildOutput* FindAtlas(const RuntimeShaderArtifact& artifact, const std::string& fontId)
{
	for (const FontAtlasBuildOutput& atlas : artifact.fontAtlases)
	{
		if (atlas.fontId == fontId)
			return &atlas;
	}
	return nullptr;
}

const ShaderParameterValue* FindParameterValue(const RuntimeShaderArtifact& artifact, const std::string& parameterId)
{
	const auto valueIt = artifact.parameterValues.find(parameterId);
	return valueIt == artifact.parameterValues.end() ? nullptr : &valueIt->second;
}

std::string TextValueForDefinition(const RuntimeShaderArtifact& artifact, const ShaderParameterDefinition& definition)
{
	const ShaderParameterValue* value = FindParameterValue(artifact, definition.id);
	return value ? value->textValue : definition.defaultTextValue;
}

void SampleAtlasPixel(const FontAtlasBuildOutput& atlas, double x, double y, unsigned char* rgba)
{
	const double clampedX = (std::max)(0.0, (std::min)(static_cast<double>(atlas.width) - 1.0, x));
	const double clampedY = (std::max)(0.0, (std::min)(static_cast<double>(atlas.height) - 1.0, y));
	const int x0 = static_cast<int>(std::floor(clampedX));
	const int y0 = static_cast<int>(std::floor(clampedY));
	const int x1 = (std::min)(static_cast<int>(atlas.width) - 1, x0 + 1);
	const int y1 = (std::min)(static_cast<int>(atlas.height) - 1, y0 + 1);
	const double tx = clampedX - static_cast<double>(x0);
	const double ty = clampedY - static_cast<double>(y0);

	const auto channelAt = [&atlas](int sx, int sy, unsigned channel) {
		const std::size_t pixelOffset = (static_cast<std::size_t>(sy) * atlas.width + static_cast<std::size_t>(sx)) * 4u;
		return static_cast<double>(atlas.rgbaPixels[pixelOffset + channel]);
	};
	for (unsigned channel = 0; channel < 4u; ++channel)
	{
		const double top = channelAt(x0, y0, channel) * (1.0 - tx) + channelAt(x1, y0, channel) * tx;
		const double bottom = channelAt(x0, y1, channel) * (1.0 - tx) + channelAt(x1, y1, channel) * tx;
		const double value = top * (1.0 - ty) + bottom * ty;
		rgba[channel] = static_cast<unsigned char>((std::max)(0.0, (std::min)(255.0, std::round(value))));
	}
}

std::vector<unsigned char> ComposeTextTexture(
	const FontAtlasBuildOutput& atlas,
	const ShaderParameterDefinition& definition,
	const std::string& text,
	unsigned& width,
	unsigned& height,
	unsigned& liveWidth)
{
	double advance = 0.0;
	for (unsigned char character : text)
	{
		const auto glyphIt = atlas.glyphsByCodepoint.find(character);
		if (glyphIt != atlas.glyphsByCodepoint.end())
			advance += glyphIt->second.advance;
	}

	const unsigned maxLength = definition.maxLength == 0 ? 64 : definition.maxLength;
	const unsigned fixedWidth = static_cast<unsigned>(std::ceil(static_cast<double>(maxLength) * kFontPixelsPerEm * 0.9)) + kTextTexturePadding * 2u;
	liveWidth = (std::max)(1u, static_cast<unsigned>(std::ceil(advance * kFontPixelsPerEm)) + kTextTexturePadding * 2u);
	width = (std::max)(fixedWidth, liveWidth);
	height = kTextTextureHeight;
	std::vector<unsigned char> texturePixels(static_cast<std::size_t>(width) * height * 4u, 0);

	const double baseline = kTextTexturePadding + (-atlas.ascender * kFontPixelsPerEm);
	double penX = static_cast<double>(kTextTexturePadding);

	for (unsigned char character : text)
	{
		const auto glyphIt = atlas.glyphsByCodepoint.find(character);
		if (glyphIt == atlas.glyphsByCodepoint.end())
			continue;

		const FontAtlasBuildOutput::Glyph& glyph = glyphIt->second;
		if (glyph.hasBounds)
		{
			const int destLeft = static_cast<int>(std::floor(penX + glyph.planeLeft * kFontPixelsPerEm));
			const int destTop = static_cast<int>(std::floor(baseline + glyph.planeTop * kFontPixelsPerEm));
			const int destRight = static_cast<int>(std::ceil(penX + glyph.planeRight * kFontPixelsPerEm));
			const int destBottom = static_cast<int>(std::ceil(baseline + glyph.planeBottom * kFontPixelsPerEm));
			const double destWidth = (std::max)(1.0, static_cast<double>(destRight - destLeft));
			const double destHeight = (std::max)(1.0, static_cast<double>(destBottom - destTop));

			for (int y = destTop; y < destBottom; ++y)
			{
				if (y < 0 || y >= static_cast<int>(height))
					continue;
				const double v = (static_cast<double>(y) + 0.5 - destTop) / destHeight;
				for (int x = destLeft; x < destRight; ++x)
				{
					if (x < 0 || x >= static_cast<int>(width))
						continue;
					const double u = (static_cast<double>(x) + 0.5 - destLeft) / destWidth;
					const double atlasX = glyph.atlasLeft + u * (glyph.atlasRight - glyph.atlasLeft);
					const double atlasY = glyph.atlasTop + v * (glyph.atlasBottom - glyph.atlasTop);
					unsigned char sample[4] = {};
					SampleAtlasPixel(atlas, atlasX, atlasY, sample);
					unsigned char* destination = texturePixels.data() + (static_cast<std::size_t>(y) * width + static_cast<std::size_t>(x)) * 4u;
					for (unsigned channel = 0; channel < 4u; ++channel)
						destination[channel] = (std::max)(destination[channel], sample[channel]);
				}
			}
		}

		penX += glyph.advance * kFontPixelsPerEm;
	}

	for (unsigned y = 0; y < height / 2u; ++y)
	{
		unsigned char* topRow = texturePixels.data() + static_cast<std::size_t>(y) * width * 4u;
		unsigned char* bottomRow = texturePixels.data() + static_cast<std::size_t>(height - 1u - y) * width * 4u;
		for (unsigned x = 0; x < width * 4u; ++x)
			std::swap(topRow[x], bottomRow[x]);
	}

	return texturePixels;
}
}

bool PrepareRuntimeTextTextures(RuntimeShaderArtifact& artifact, std::string& error)
{
	artifact.preparedTextTextures.clear();
	artifact.textTextureMetrics.clear();

	for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
	{
		if (definition.type != ShaderParameterType::Text)
			continue;

		const FontAtlasBuildOutput* atlas = FindAtlas(artifact, definition.fontId);
		if (atlas == nullptr)
		{
			error = "No prepared font atlas is available for text parameter '" + definition.id + "'.";
			return false;
		}
		if (atlas->width == 0 || atlas->height == 0 || atlas->rgbaPixels.empty() || atlas->glyphsByCodepoint.empty())
		{
			error = "Prepared font atlas data is empty for font '" + definition.fontId + "'.";
			return false;
		}

		RuntimePreparedTextTexture prepared;
		prepared.parameterId = definition.id;
		prepared.textValue = TextValueForDefinition(artifact, definition);
		std::vector<unsigned char> pixels = ComposeTextTexture(*atlas, definition, prepared.textValue, prepared.width, prepared.height, prepared.liveWidth);
		if (pixels.empty() || prepared.width == 0 || prepared.height == 0)
		{
			error = "Could not prepare text texture for parameter '" + definition.id + "'.";
			return false;
		}
		prepared.rgbaPixels = std::make_shared<const std::vector<unsigned char>>(std::move(pixels));

		RuntimeTextTextureMetrics metrics;
		metrics.activeWidthScale = prepared.width == 0
			? 1.0f
			: static_cast<float>(prepared.liveWidth) / static_cast<float>(prepared.width);
		metrics.aspect = prepared.height == 0
			? 1.0f
			: static_cast<float>(prepared.liveWidth) / static_cast<float>(prepared.height);
		artifact.textTextureMetrics[prepared.parameterId] = metrics;
		artifact.preparedTextTextures.push_back(std::move(prepared));
	}

	error.clear();
	return true;
}
}
