#include "RuntimeRenderScene.h"

#include <string>
#include <vector>

#ifndef GL_FRAMEBUFFER_BINDING
#define GL_FRAMEBUFFER_BINDING 0x8CA6
#endif

bool RuntimeRenderScene::HasLayers()
{
	ConsumePreparedPrograms();

	for (const std::string& layerId : mLayerOrder)
	{
		const LayerProgram* layer = FindLayer(layerId);
		if (!layer)
			continue;
		for (const LayerProgram::PassProgram& pass : layer->passes)
		{
			if (pass.renderer && pass.renderer->HasProgram())
				return true;
		}
	}
	return false;
}

void RuntimeRenderScene::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint videoInputTexture)
{
	ConsumePreparedPrograms();

	std::vector<LayerProgram*> readyLayers;
	for (const std::string& layerId : mLayerOrder)
	{
		LayerProgram* layer = FindLayer(layerId);
		if (!layer)
			continue;
		for (const LayerProgram::PassProgram& pass : layer->passes)
		{
			if (pass.renderer && pass.renderer->HasProgram())
			{
				readyLayers.push_back(layer);
				break;
			}
		}
	}

	if (readyLayers.empty())
		return;

	GLint outputFramebuffer = 0;
	glGetIntegerv(GL_FRAMEBUFFER_BINDING, &outputFramebuffer);

	if (readyLayers.size() == 1)
	{
		RenderLayer(*readyLayers.front(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
		return;
	}

	if (!EnsureLayerTargets(width, height))
	{
		glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
		RenderLayer(*readyLayers.back(), frameIndex, width, height, videoInputTexture, videoInputTexture, static_cast<GLuint>(outputFramebuffer), true);
		return;
	}

	// Shader source contract:
	// - gVideoInput is the decoded current input texture for every layer in the stack.
	// - gLayerInput starts as gVideoInput for the first layer, then becomes the previous layer output.
	GLuint layerInputTexture = videoInputTexture;
	std::size_t nextTargetIndex = 0;
	for (std::size_t layerIndex = 0; layerIndex < readyLayers.size(); ++layerIndex)
	{
		const bool isFinalLayer = layerIndex == readyLayers.size() - 1;
		if (isFinalLayer)
		{
			glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(outputFramebuffer));
			RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, static_cast<GLuint>(outputFramebuffer), true);
			continue;
		}

		RenderLayer(*readyLayers[layerIndex], frameIndex, width, height, videoInputTexture, layerInputTexture, mLayerFramebuffers[nextTargetIndex], true);
		layerInputTexture = mLayerTextures[nextTargetIndex];
		nextTargetIndex = 1 - nextTargetIndex;
	}
}

GLuint RuntimeRenderScene::RenderLayer(
	LayerProgram& layer,
	uint64_t frameIndex,
	unsigned width,
	unsigned height,
	GLuint videoInputTexture,
	GLuint layerInputTexture,
	GLuint outputFramebuffer,
	bool renderToOutput)
{
	GLuint namedOutputs[2] = {};
	std::string namedOutputNames[2];
	std::size_t nextTargetIndex = 2;
	GLuint lastOutputTexture = layerInputTexture;

	for (LayerProgram::PassProgram& pass : layer.passes)
	{
		if (!pass.renderer || !pass.renderer->HasProgram())
			continue;

		GLuint sourceTexture = layerInputTexture;
		if (!pass.inputNames.empty())
		{
			const std::string& inputName = pass.inputNames.front();
			if (inputName == "videoInput")
			{
				sourceTexture = videoInputTexture;
			}
			else if (inputName == "layerInput")
			{
				sourceTexture = layerInputTexture;
			}
			else
			{
				// Named intermediate pass inputs currently use the gVideoInput binding slot as the
				// selected pass source. Layer stack shaders should use gLayerInput for previous-layer
				// sampling and gVideoInput for the original input frame.
				for (std::size_t index = 0; index < 2; ++index)
				{
					if (namedOutputNames[index] == inputName)
					{
						sourceTexture = namedOutputs[index];
						break;
					}
				}
			}
		}

		const bool writesLayerOutput = pass.outputName == "layerOutput";
		if (writesLayerOutput && renderToOutput)
		{
			glBindFramebuffer(GL_FRAMEBUFFER, outputFramebuffer);
			pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
			lastOutputTexture = 0;
			continue;
		}

		if (!EnsureLayerTargets(width, height))
			continue;

		const std::size_t targetIndex = nextTargetIndex;
		nextTargetIndex = nextTargetIndex == 2 ? 3 : 2;
		glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[targetIndex]);
		pass.renderer->RenderFrame(frameIndex, width, height, sourceTexture, layerInputTexture);
		const std::size_t namedIndex = targetIndex - 2;
		namedOutputs[namedIndex] = mLayerTextures[targetIndex];
		namedOutputNames[namedIndex] = pass.outputName;
		lastOutputTexture = mLayerTextures[targetIndex];
	}

	return lastOutputTexture;
}

bool RuntimeRenderScene::EnsureLayerTargets(unsigned width, unsigned height)
{
	if (width == 0 || height == 0)
		return false;
	if (mLayerFramebuffers[0] != 0 && mLayerFramebuffers[1] != 0 && mLayerFramebuffers[2] != 0 && mLayerFramebuffers[3] != 0
		&& mLayerTextures[0] != 0 && mLayerTextures[1] != 0 && mLayerTextures[2] != 0 && mLayerTextures[3] != 0
		&& mLayerTargetWidth == width && mLayerTargetHeight == height)
		return true;

	DestroyLayerTargets();
	mLayerTargetWidth = width;
	mLayerTargetHeight = height;

	glGenFramebuffers(4, mLayerFramebuffers);
	glGenTextures(4, mLayerTextures);
	for (int index = 0; index < 4; ++index)
	{
		glBindTexture(GL_TEXTURE_2D, mLayerTextures[index]);
		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>(width),
			static_cast<GLsizei>(height),
			0,
			GL_BGRA,
			GL_UNSIGNED_INT_8_8_8_8_REV,
			nullptr);

		glBindFramebuffer(GL_FRAMEBUFFER, mLayerFramebuffers[index]);
		glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, mLayerTextures[index], 0);
		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
		{
			glBindFramebuffer(GL_FRAMEBUFFER, 0);
			glBindTexture(GL_TEXTURE_2D, 0);
			DestroyLayerTargets();
			return false;
		}
	}

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindTexture(GL_TEXTURE_2D, 0);
	return true;
}

void RuntimeRenderScene::DestroyLayerTargets()
{
	if (mLayerFramebuffers[0] != 0 || mLayerFramebuffers[1] != 0 || mLayerFramebuffers[2] != 0 || mLayerFramebuffers[3] != 0)
		glDeleteFramebuffers(4, mLayerFramebuffers);
	if (mLayerTextures[0] != 0 || mLayerTextures[1] != 0 || mLayerTextures[2] != 0 || mLayerTextures[3] != 0)
		glDeleteTextures(4, mLayerTextures);
	for (int index = 0; index < 4; ++index)
	{
		mLayerFramebuffers[index] = 0;
		mLayerTextures[index] = 0;
	}
	mLayerTargetWidth = 0;
	mLayerTargetHeight = 0;
}
