#include "ShaderTextureBindings.h"

#include "GlRenderConstants.h"
#include "TextRasterizer.h"
#include "TextureAssetLoader.h"

#include <algorithm>
#include <filesystem>

namespace
{
std::string TextValueForBinding(const RuntimeRenderState& state, const std::string& parameterId)
{
	auto valueIt = state.parameterValues.find(parameterId);
	return valueIt == state.parameterValues.end() ? std::string() : valueIt->second.textValue;
}

const ShaderFontAsset* FindFontAssetForParameter(const RuntimeRenderState& state, const ShaderParameterDefinition& definition)
{
	if (!definition.fontId.empty())
	{
		for (const ShaderFontAsset& fontAsset : state.fontAssets)
		{
			if (fontAsset.id == definition.fontId)
				return &fontAsset;
		}
	}
	return state.fontAssets.empty() ? nullptr : &state.fontAssets.front();
}
}

bool ShaderTextureBindings::LoadTextureAsset(const ShaderTextureAsset& textureAsset, GLuint& textureId, std::string& error)
{
	return ::LoadTextureAsset(textureAsset, textureId, error);
}

void ShaderTextureBindings::CreateTextBindings(const RuntimeRenderState& state, std::vector<LayerProgram::TextBinding>& textBindings)
{
	for (const ShaderParameterDefinition& definition : state.parameterDefinitions)
	{
		if (definition.type != ShaderParameterType::Text)
			continue;
		LayerProgram::TextBinding textBinding;
		textBinding.parameterId = definition.id;
		textBinding.samplerName = definition.id + "Texture";
		textBinding.fontId = definition.fontId;
		glGenTextures(1, &textBinding.texture);
		glBindTexture(GL_TEXTURE_2D, textBinding.texture);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
		std::vector<unsigned char> empty(static_cast<std::size_t>(kTextTextureWidth) * kTextTextureHeight * 4, 0);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, kTextTextureWidth, kTextTextureHeight, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, empty.data());
		glBindTexture(GL_TEXTURE_2D, 0);
		textBindings.push_back(textBinding);
	}
}

bool ShaderTextureBindings::UpdateTextBindingTexture(const RuntimeRenderState& state, LayerProgram::TextBinding& textBinding, std::string& error)
{
	const std::string text = TextValueForBinding(state, textBinding.parameterId);
	if (text == textBinding.renderedText && textBinding.renderedWidth == kTextTextureWidth && textBinding.renderedHeight == kTextTextureHeight)
		return true;

	auto definitionIt = std::find_if(state.parameterDefinitions.begin(), state.parameterDefinitions.end(),
		[&textBinding](const ShaderParameterDefinition& definition) { return definition.id == textBinding.parameterId; });
	if (definitionIt == state.parameterDefinitions.end())
		return true;

	const ShaderFontAsset* fontAsset = FindFontAssetForParameter(state, *definitionIt);
	std::filesystem::path fontPath;
	if (fontAsset)
		fontPath = fontAsset->path;

	std::vector<unsigned char> sdf;
	if (!RasterizeTextSdf(text, fontPath, sdf, error))
		return false;

	GLint previousActiveTexture = 0;
	GLint previousUnpackBuffer = 0;
	glGetIntegerv(GL_ACTIVE_TEXTURE, &previousActiveTexture);
	glGetIntegerv(GL_PIXEL_UNPACK_BUFFER_BINDING, &previousUnpackBuffer);
	glActiveTexture(GL_TEXTURE0);
	glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
	glBindTexture(GL_TEXTURE_2D, textBinding.texture);
	glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, kTextTextureWidth, kTextTextureHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, sdf.data());
	glBindTexture(GL_TEXTURE_2D, 0);
	glBindBuffer(GL_PIXEL_UNPACK_BUFFER, static_cast<GLuint>(previousUnpackBuffer));
	glActiveTexture(static_cast<GLenum>(previousActiveTexture));

	textBinding.renderedText = text;
	textBinding.renderedWidth = kTextTextureWidth;
	textBinding.renderedHeight = kTextTextureHeight;
	return true;
}

GLint ShaderTextureBindings::FindSamplerUniformLocation(GLuint program, const std::string& samplerName) const
{
	GLint location = glGetUniformLocation(program, samplerName.c_str());
	if (location >= 0)
		return location;
	return glGetUniformLocation(program, (samplerName + "_0").c_str());
}

GLuint ShaderTextureBindings::ResolveFeedbackTextureUnit(const RuntimeRenderState& state, unsigned historyCap) const
{
	return state.isTemporal ? kSourceHistoryTextureUnitBase + historyCap + historyCap : kSourceHistoryTextureUnitBase;
}

GLuint ShaderTextureBindings::ResolveShaderTextureBase(const RuntimeRenderState& state, unsigned historyCap) const
{
	return ResolveFeedbackTextureUnit(state, historyCap) + (state.feedback.enabled ? 1u : 0u);
}

void ShaderTextureBindings::AssignLayerSamplerUniforms(GLuint program, const RuntimeRenderState& state, const PassProgram& passProgram, unsigned historyCap) const
{
	const GLuint shaderTextureBase = ResolveShaderTextureBase(state, historyCap);

	const GLint layerInputLocation = FindSamplerUniformLocation(program, "gLayerInput");
	if (layerInputLocation >= 0)
		glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));

	const GLint videoInputLocation = FindSamplerUniformLocation(program, "gVideoInput");
	if (videoInputLocation >= 0)
		glUniform1i(videoInputLocation, static_cast<GLint>(kDecodedVideoTextureUnit));

	for (unsigned index = 0; index < historyCap; ++index)
	{
		const std::string sourceSamplerName = "gSourceHistory" + std::to_string(index);
		const GLint sourceSamplerLocation = glGetUniformLocation(program, sourceSamplerName.c_str());
		if (sourceSamplerLocation >= 0)
			glUniform1i(sourceSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + index));

		const std::string temporalSamplerName = "gTemporalHistory" + std::to_string(index);
		const GLint temporalSamplerLocation = glGetUniformLocation(program, temporalSamplerName.c_str());
		if (temporalSamplerLocation >= 0)
			glUniform1i(temporalSamplerLocation, static_cast<GLint>(kSourceHistoryTextureUnitBase + historyCap + index));
	}

	if (state.feedback.enabled)
	{
		const GLint feedbackSamplerLocation = FindSamplerUniformLocation(program, "gFeedbackState");
		if (feedbackSamplerLocation >= 0)
			glUniform1i(feedbackSamplerLocation, static_cast<GLint>(ResolveFeedbackTextureUnit(state, historyCap)));
	}

	for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
	{
		const GLint textureSamplerLocation = FindSamplerUniformLocation(program, passProgram.textureBindings[index].samplerName);
		if (textureSamplerLocation >= 0)
			glUniform1i(textureSamplerLocation, static_cast<GLint>(shaderTextureBase + static_cast<GLuint>(index)));
	}

	const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
	for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
	{
		const GLint textSamplerLocation = FindSamplerUniformLocation(program, passProgram.textBindings[index].samplerName);
		if (textSamplerLocation >= 0)
			glUniform1i(textSamplerLocation, static_cast<GLint>(textTextureBase + static_cast<GLuint>(index)));
	}
}

ShaderTextureBindings::RuntimeTextureBindingPlan ShaderTextureBindings::BuildLayerRuntimeBindingPlan(
	const PassProgram& passProgram,
	GLuint layerInputTexture,
	GLuint originalLayerInputTexture,
	const RuntimeRenderState& state,
	GLuint feedbackTexture,
	const std::vector<GLuint>& sourceHistoryTextures,
	const std::vector<GLuint>& temporalHistoryTextures) const
{
	RuntimeTextureBindingPlan plan;
	plan.bindings.push_back({ "originalLayerInput", "gLayerInput", originalLayerInputTexture, kLayerInputTextureUnit });
	plan.bindings.push_back({ "layerInput", "gVideoInput", layerInputTexture, kDecodedVideoTextureUnit });

	for (std::size_t index = 0; index < sourceHistoryTextures.size(); ++index)
	{
		plan.bindings.push_back({
			"sourceHistory",
			"gSourceHistory" + std::to_string(index),
			sourceHistoryTextures[index],
			kSourceHistoryTextureUnitBase + static_cast<GLuint>(index)
		});
	}

	const GLuint temporalBase = kSourceHistoryTextureUnitBase + static_cast<GLuint>(sourceHistoryTextures.size());
	for (std::size_t index = 0; index < temporalHistoryTextures.size(); ++index)
	{
		plan.bindings.push_back({
			"temporalHistory",
			"gTemporalHistory" + std::to_string(index),
			temporalHistoryTextures[index],
			temporalBase + static_cast<GLuint>(index)
		});
	}

	const GLuint feedbackTextureUnit = ResolveFeedbackTextureUnit(state, static_cast<unsigned>(sourceHistoryTextures.size()));
	if (state.feedback.enabled)
	{
		plan.bindings.push_back({
			"feedbackState",
			"gFeedbackState",
			feedbackTexture,
			feedbackTextureUnit
		});
	}

	const GLuint shaderTextureBase = passProgram.shaderTextureBase != 0
		? passProgram.shaderTextureBase
		: feedbackTextureUnit + (state.feedback.enabled ? 1u : 0u);
	for (std::size_t index = 0; index < passProgram.textureBindings.size(); ++index)
	{
		const LayerProgram::TextureBinding& textureBinding = passProgram.textureBindings[index];
		plan.bindings.push_back({
			"shaderTexture",
			textureBinding.samplerName,
			textureBinding.texture,
			shaderTextureBase + static_cast<GLuint>(index)
		});
	}

	const GLuint textTextureBase = shaderTextureBase + static_cast<GLuint>(passProgram.textureBindings.size());
	for (std::size_t index = 0; index < passProgram.textBindings.size(); ++index)
	{
		const LayerProgram::TextBinding& textBinding = passProgram.textBindings[index];
		plan.bindings.push_back({
			"textTexture",
			textBinding.samplerName,
			textBinding.texture,
			textTextureBase + static_cast<GLuint>(index)
		});
	}

	return plan;
}

void ShaderTextureBindings::BindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
{
	for (const RuntimeTextureBinding& binding : plan.bindings)
	{
		glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
		glBindTexture(GL_TEXTURE_2D, binding.texture);
	}
	glActiveTexture(GL_TEXTURE0);
}

void ShaderTextureBindings::UnbindRuntimeTexturePlan(const RuntimeTextureBindingPlan& plan) const
{
	for (const RuntimeTextureBinding& binding : plan.bindings)
	{
		glActiveTexture(GL_TEXTURE0 + binding.textureUnit);
		glBindTexture(GL_TEXTURE_2D, 0);
	}
	glActiveTexture(GL_TEXTURE0);
}
