#include "RuntimeTextTextureCache.h"

namespace
{
constexpr GLuint kFirstTextTextureUnit = 8;
}

RuntimeTextTextureCache::~RuntimeTextTextureCache()
{
	ShutdownGl();
}

bool RuntimeTextTextureCache::Configure(const RuntimeShaderArtifact& artifact, std::string& error)
{
	ShutdownGl();
	mArtifact = artifact;

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

		if (FindPreparedTexture(definition.id) == nullptr)
		{
			error = "No prepared text texture is available for text parameter '" + definition.id + "'.";
			return false;
		}

		TextTexture texture;
		texture.parameterId = definition.id;
		texture.maxLength = definition.maxLength == 0 ? 64 : definition.maxLength;
		mTextTextures.push_back(std::move(texture));
	}

	error.clear();
	return true;
}

void RuntimeTextTextureCache::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
{
	mArtifact.parameterDefinitions = artifact.parameterDefinitions;
	mArtifact.parameterValues = artifact.parameterValues;
	mArtifact.textTextureMetrics = artifact.textTextureMetrics;
	mArtifact.preparedTextTextures = artifact.preparedTextTextures;
}

void RuntimeTextTextureCache::RefreshTextTextures(RuntimeShaderArtifact* artifactState)
{
	for (TextTexture& textTexture : mTextTextures)
		EnsureTextTexture(textTexture);
	if (artifactState)
		artifactState->textTextureMetrics = mArtifact.textTextureMetrics;
}

void RuntimeTextTextureCache::BindTextTextures(GLuint program)
{
	for (std::size_t index = 0; index < mTextTextures.size(); ++index)
	{
		const TextTexture& textTexture = mTextTextures[index];
		if (textTexture.texture == 0)
			continue;
		glActiveTexture(GL_TEXTURE0 + kFirstTextTextureUnit + static_cast<GLuint>(index));
		glBindTexture(GL_TEXTURE_2D, textTexture.texture);
	}

	glUseProgram(program);
	AssignSamplerUniforms(program, mArtifact);
	glUseProgram(0);
	glActiveTexture(GL_TEXTURE0);
}

void RuntimeTextTextureCache::ShutdownGl()
{
	for (TextTexture& texture : mTextTextures)
		DestroyTexture(texture);
	mTextTextures.clear();
}

void RuntimeTextTextureCache::AssignSamplerUniforms(GLuint program, const RuntimeShaderArtifact& artifact)
{
	glUseProgram(program);
	GLuint nextUnit = kFirstTextTextureUnit;
	for (const ShaderParameterDefinition& definition : artifact.parameterDefinitions)
	{
		if (definition.type != ShaderParameterType::Text)
			continue;

		const std::string samplerName = definition.id + "Texture";
		const GLint location = glGetUniformLocation(program, samplerName.c_str());
		if (location >= 0)
			glUniform1i(location, static_cast<GLint>(nextUnit));
		const std::string samplerArrayName = samplerName + "_0";
		const GLint arrayLocation = glGetUniformLocation(program, samplerArrayName.c_str());
		if (arrayLocation >= 0)
			glUniform1i(arrayLocation, static_cast<GLint>(nextUnit));
		++nextUnit;
	}
	glUseProgram(0);
}

bool RuntimeTextTextureCache::EnsureTextTexture(TextTexture& texture)
{
	const RuntimePreparedTextTexture* prepared = FindPreparedTexture(texture.parameterId);
	if (!prepared || !prepared->rgbaPixels || prepared->rgbaPixels->empty() || prepared->width == 0 || prepared->height == 0)
		return false;
	if (texture.texture != 0 && texture.cachedText == prepared->textValue && texture.width == prepared->width && texture.height == prepared->height)
		return true;

	if (texture.texture == 0)
		glGenTextures(1, &texture.texture);

	GLint previousUnpackAlignment = 4;
	glGetIntegerv(GL_UNPACK_ALIGNMENT, &previousUnpackAlignment);
	glBindTexture(GL_TEXTURE_2D, texture.texture);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	if (texture.width != prepared->width || texture.height != prepared->height)
	{
		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);
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, static_cast<GLsizei>(prepared->width), static_cast<GLsizei>(prepared->height), 0, GL_RGBA, GL_UNSIGNED_BYTE, prepared->rgbaPixels->data());
	}
	else
	{
		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, static_cast<GLsizei>(prepared->width), static_cast<GLsizei>(prepared->height), GL_RGBA, GL_UNSIGNED_BYTE, prepared->rgbaPixels->data());
	}
	glBindTexture(GL_TEXTURE_2D, 0);
	glPixelStorei(GL_UNPACK_ALIGNMENT, previousUnpackAlignment);
	glActiveTexture(GL_TEXTURE0);

	texture.cachedText = prepared->textValue;
	texture.width = prepared->width;
	texture.height = prepared->height;
	texture.liveWidth = prepared->liveWidth;
	return texture.texture != 0;
}

const RuntimePreparedTextTexture* RuntimeTextTextureCache::FindPreparedTexture(const std::string& parameterId) const
{
	for (const RuntimePreparedTextTexture& prepared : mArtifact.preparedTextTextures)
	{
		if (prepared.parameterId == parameterId)
			return &prepared;
	}
	return nullptr;
}

void RuntimeTextTextureCache::DestroyTexture(TextTexture& texture)
{
	if (texture.texture != 0)
		glDeleteTextures(1, &texture.texture);
	texture.texture = 0;
	texture.width = 0;
	texture.height = 0;
	texture.cachedText.clear();
}
