#include "RuntimeShaderRenderer.h"

#include "RuntimeShaderParams.h"

#include <array>
#include <cstring>
#include <string>

namespace
{
constexpr GLuint kGlobalParamsBindingPoint = 0;
constexpr GLuint kVideoInputTextureUnit = 0;
constexpr GLuint kLayerInputTextureUnit = 1;

const char* kVertexShaderSource = R"GLSL(
#version 430 core
out vec2 vTexCoord;
void main()
{
	vec2 positions[3] = vec2[3](
		vec2(-1.0, -1.0),
		vec2( 3.0, -1.0),
		vec2(-1.0,  3.0));
	vec2 texCoords[3] = vec2[3](
		vec2(0.0, 0.0),
		vec2(2.0, 0.0),
		vec2(0.0, 2.0));
	gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
	vTexCoord = texCoords[gl_VertexID];
}
)GLSL";

}

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

bool RuntimeShaderRenderer::CommitPreparedProgram(RuntimePreparedShaderProgram& preparedProgram, std::string& error)
{
	if (!preparedProgram.succeeded || preparedProgram.program == 0)
	{
		error = preparedProgram.error.empty() ? "Prepared runtime shader program is not valid." : preparedProgram.error;
		return false;
	}

	if (!EnsureStaticGlResources(error))
		return false;

	DestroyProgram();
	mProgram = preparedProgram.program;
	mVertexShader = preparedProgram.vertexShader;
	mFragmentShader = preparedProgram.fragmentShader;
	mArtifact = preparedProgram.artifact;
	preparedProgram.program = 0;
	preparedProgram.vertexShader = 0;
	preparedProgram.fragmentShader = 0;
	if (!mTextTextures.Configure(mArtifact, error))
	{
		DestroyProgram();
		return false;
	}
	mTextTextures.RefreshTextTextures(&mArtifact);
	return true;
}

void RuntimeShaderRenderer::UpdateArtifactState(const RuntimeShaderArtifact& artifact)
{
	mArtifact.parameterDefinitions = artifact.parameterDefinitions;
	mArtifact.parameterValues = artifact.parameterValues;
	mArtifact.message = artifact.message;
	mTextTextures.UpdateArtifactState(artifact);
	mTextTextures.RefreshTextTextures(&mArtifact);
}

bool RuntimeShaderRenderer::BuildPreparedProgram(
	const std::string& layerId,
	const std::string& sourceFingerprint,
	const RuntimeShaderArtifact& artifact,
	RuntimePreparedShaderProgram& preparedProgram)
{
	RuntimeShaderPassArtifact passArtifact;
	passArtifact.passId = "main";
	passArtifact.fragmentShaderSource = artifact.fragmentShaderSource;
	passArtifact.outputName = "layerOutput";
	if (!artifact.passes.empty())
		passArtifact = artifact.passes.front();
	return BuildPreparedPassProgram(layerId, sourceFingerprint, artifact, passArtifact, preparedProgram);
}

bool RuntimeShaderRenderer::BuildPreparedPassProgram(
	const std::string& layerId,
	const std::string& sourceFingerprint,
	const RuntimeShaderArtifact& artifact,
	const RuntimeShaderPassArtifact& passArtifact,
	RuntimePreparedShaderProgram& preparedProgram)
{
	preparedProgram = RuntimePreparedShaderProgram();
	preparedProgram.layerId = layerId;
	preparedProgram.shaderId = artifact.shaderId;
	preparedProgram.passId = passArtifact.passId;
	preparedProgram.sourceFingerprint = sourceFingerprint;
	preparedProgram.artifact = artifact;
	preparedProgram.passArtifact = passArtifact;
	preparedProgram.inputNames = passArtifact.inputNames;
	preparedProgram.outputName = passArtifact.outputName.empty() ? passArtifact.passId : passArtifact.outputName;

	if (passArtifact.fragmentShaderSource.empty())
	{
		preparedProgram.error = "Cannot prepare an empty fragment shader.";
		return false;
	}

	if (!BuildProgram(
		passArtifact.fragmentShaderSource,
		preparedProgram.program,
		preparedProgram.vertexShader,
		preparedProgram.fragmentShader,
		preparedProgram.error))
	{
		preparedProgram.ReleaseGl();
		return false;
	}

	preparedProgram.succeeded = true;
	AssignSamplerUniforms(preparedProgram.program);
	RuntimeTextTextureCache::AssignSamplerUniforms(preparedProgram.program, artifact);
	return true;
}

void RuntimeShaderRenderer::RenderFrame(uint64_t frameIndex, unsigned width, unsigned height, GLuint sourceTexture, GLuint layerInputTexture)
{
	if (mProgram == 0)
		return;

	glViewport(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height));
	glDisable(GL_SCISSOR_TEST);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_BLEND);
	UpdateGlobalParams(frameIndex, width, height);
	BindRuntimeTextures(sourceTexture, layerInputTexture);
	mTextTextures.BindTextTextures(mProgram);
	glBindVertexArray(mVertexArray);
	glUseProgram(mProgram);
	glDrawArrays(GL_TRIANGLES, 0, 3);
	glUseProgram(0);
	glBindVertexArray(0);
}

void RuntimeShaderRenderer::ShutdownGl()
{
	DestroyProgram();
	DestroyStaticGlResources();
}

bool RuntimeShaderRenderer::EnsureStaticGlResources(std::string& error)
{
	if (mVertexArray == 0)
		glGenVertexArrays(1, &mVertexArray);
	if (mGlobalParamsBuffer == 0)
	{
		glGenBuffers(1, &mGlobalParamsBuffer);
		glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
		glBufferData(GL_UNIFORM_BUFFER, 1024, nullptr, GL_DYNAMIC_DRAW);
		glBindBuffer(GL_UNIFORM_BUFFER, 0);
	}
	if (mFallbackSourceTexture == 0)
	{
		const unsigned char pixels[] = {
			0, 0, 0, 255,
			96, 64, 32, 255,
			64, 96, 160, 255,
			255, 255, 255, 255
		};
		glGenTextures(1, &mFallbackSourceTexture);
		glBindTexture(GL_TEXTURE_2D, mFallbackSourceTexture);
		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, 2, 2, 0, GL_BGRA, GL_UNSIGNED_BYTE, pixels);
		glBindTexture(GL_TEXTURE_2D, 0);
	}

	if (mVertexArray == 0 || mGlobalParamsBuffer == 0 || mFallbackSourceTexture == 0)
	{
		error = "Failed to create runtime shader GL resources.";
		return false;
	}
	return true;
}

bool RuntimeShaderRenderer::BuildProgram(const std::string& fragmentShaderSource, GLuint& program, GLuint& vertexShader, GLuint& fragmentShader, std::string& error)
{
	program = 0;
	vertexShader = 0;
	fragmentShader = 0;

	if (!CompileShader(GL_VERTEX_SHADER, kVertexShaderSource, vertexShader, error))
		return false;
	if (!CompileShader(GL_FRAGMENT_SHADER, fragmentShaderSource.c_str(), fragmentShader, error))
	{
		glDeleteShader(vertexShader);
		vertexShader = 0;
		return false;
	}

	program = glCreateProgram();
	glAttachShader(program, vertexShader);
	glAttachShader(program, fragmentShader);
	glLinkProgram(program);

	GLint linkResult = GL_FALSE;
	glGetProgramiv(program, GL_LINK_STATUS, &linkResult);
	if (linkResult == GL_FALSE)
	{
		std::array<char, 4096> log = {};
		GLsizei length = 0;
		glGetProgramInfoLog(program, static_cast<GLsizei>(log.size()), &length, log.data());
		error = std::string(log.data(), static_cast<std::size_t>(length));
		glDeleteProgram(program);
		glDeleteShader(vertexShader);
		glDeleteShader(fragmentShader);
		program = 0;
		vertexShader = 0;
		fragmentShader = 0;
		return false;
	}

	const GLuint globalParamsIndex = glGetUniformBlockIndex(program, "GlobalParams");
	if (globalParamsIndex != GL_INVALID_INDEX)
		glUniformBlockBinding(program, globalParamsIndex, kGlobalParamsBindingPoint);
	return true;
}

void RuntimeShaderRenderer::AssignSamplerUniforms(GLuint program)
{
	glUseProgram(program);
	const GLint videoInputLocation = glGetUniformLocation(program, "gVideoInput");
	if (videoInputLocation >= 0)
		glUniform1i(videoInputLocation, static_cast<GLint>(kVideoInputTextureUnit));
	const GLint videoInputArrayLocation = glGetUniformLocation(program, "gVideoInput_0");
	if (videoInputArrayLocation >= 0)
		glUniform1i(videoInputArrayLocation, static_cast<GLint>(kVideoInputTextureUnit));
	const GLint layerInputLocation = glGetUniformLocation(program, "gLayerInput");
	if (layerInputLocation >= 0)
		glUniform1i(layerInputLocation, static_cast<GLint>(kLayerInputTextureUnit));
	const GLint layerInputArrayLocation = glGetUniformLocation(program, "gLayerInput_0");
	if (layerInputArrayLocation >= 0)
		glUniform1i(layerInputArrayLocation, static_cast<GLint>(kLayerInputTextureUnit));
	glUseProgram(0);
}

void RuntimeShaderRenderer::UpdateGlobalParams(uint64_t frameIndex, unsigned width, unsigned height)
{
	std::vector<unsigned char>& buffer = mGlobalParamsScratch;
	buffer = BuildRuntimeShaderGlobalParamsStd140(mArtifact, frameIndex, width, height);
	glBindBuffer(GL_UNIFORM_BUFFER, mGlobalParamsBuffer);
	const GLsizeiptr bufferSize = static_cast<GLsizeiptr>(buffer.size());
	if (mGlobalParamsBufferSize != bufferSize)
	{
		glBufferData(GL_UNIFORM_BUFFER, bufferSize, buffer.data(), GL_DYNAMIC_DRAW);
		mGlobalParamsBufferSize = bufferSize;
	}
	else
	{
		glBufferSubData(GL_UNIFORM_BUFFER, 0, bufferSize, buffer.data());
	}
	glBindBufferBase(GL_UNIFORM_BUFFER, kGlobalParamsBindingPoint, mGlobalParamsBuffer);
	glBindBuffer(GL_UNIFORM_BUFFER, 0);
}

void RuntimeShaderRenderer::BindRuntimeTextures(GLuint sourceTexture, GLuint layerInputTexture)
{
	const GLuint resolvedSourceTexture = sourceTexture != 0 ? sourceTexture : mFallbackSourceTexture;
	const GLuint resolvedLayerInputTexture = layerInputTexture != 0 ? layerInputTexture : resolvedSourceTexture;
	glActiveTexture(GL_TEXTURE0 + kVideoInputTextureUnit);
	glBindTexture(GL_TEXTURE_2D, resolvedSourceTexture);
	glActiveTexture(GL_TEXTURE0 + kLayerInputTextureUnit);
	glBindTexture(GL_TEXTURE_2D, resolvedLayerInputTexture);
	glActiveTexture(GL_TEXTURE0);
}

bool RuntimeShaderRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader, std::string& error)
{
	shader = glCreateShader(shaderType);
	glShaderSource(shader, 1, &source, nullptr);
	glCompileShader(shader);

	GLint compileResult = GL_FALSE;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
	if (compileResult != GL_FALSE)
		return true;

	std::array<char, 4096> log = {};
	GLsizei length = 0;
	glGetShaderInfoLog(shader, static_cast<GLsizei>(log.size()), &length, log.data());
	error = std::string(log.data(), static_cast<std::size_t>(length));
	glDeleteShader(shader);
	shader = 0;
	return false;
}

void RuntimeShaderRenderer::DestroyProgram()
{
	if (mProgram != 0)
		glDeleteProgram(mProgram);
	if (mVertexShader != 0)
		glDeleteShader(mVertexShader);
	if (mFragmentShader != 0)
		glDeleteShader(mFragmentShader);
	mTextTextures.ShutdownGl();
	mProgram = 0;
	mVertexShader = 0;
	mFragmentShader = 0;
}

void RuntimeShaderRenderer::DestroyStaticGlResources()
{
	if (mGlobalParamsBuffer != 0)
		glDeleteBuffers(1, &mGlobalParamsBuffer);
	if (mVertexArray != 0)
		glDeleteVertexArrays(1, &mVertexArray);
	if (mFallbackSourceTexture != 0)
		glDeleteTextures(1, &mFallbackSourceTexture);
	mGlobalParamsBuffer = 0;
	mGlobalParamsBufferSize = 0;
	mVertexArray = 0;
	mFallbackSourceTexture = 0;
}
