#include "ShaderProgramCompiler.h"

#include "GlRenderConstants.h"
#include "GlScopedObjects.h"
#include "GlShaderSources.h"

#include <cstring>
#include <utility>
#include <vector>

namespace
{
void CopyErrorMessage(const std::string& message, int errorMessageSize, char* errorMessage)
{
	if (!errorMessage || errorMessageSize <= 0)
		return;

	strncpy_s(errorMessage, errorMessageSize, message.c_str(), _TRUNCATE);
}
}

ShaderProgramCompiler::ShaderProgramCompiler(OpenGLRenderer& renderer, RuntimeSnapshotProvider& runtimeSnapshotProvider, ShaderTextureBindings& textureBindings) :
	mRenderer(renderer),
	mRuntimeSnapshotProvider(runtimeSnapshotProvider),
	mTextureBindings(textureBindings)
{
}

bool ShaderProgramCompiler::CompileLayerProgram(const RuntimeRenderState& state, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
	std::vector<ShaderPassBuildSource> passSources;
	std::string loadError;

	if (!mRuntimeSnapshotProvider.BuildLayerPassFragmentShaderSources(state.layerId, passSources, loadError))
	{
		CopyErrorMessage(loadError, errorMessageSize, errorMessage);
		return false;
	}

	return CompilePreparedLayerProgram(state, passSources, layerProgram, errorMessageSize, errorMessage);
}

bool ShaderProgramCompiler::CompilePreparedLayerProgram(const RuntimeRenderState& state, const std::vector<ShaderPassBuildSource>& passSources, LayerProgram& layerProgram, int errorMessageSize, char* errorMessage)
{
	GLsizei errorBufferSize = 0;
	std::string loadError;
	const char* vertexSource = kFullscreenTriangleVertexShaderSource;

	layerProgram.layerId = state.layerId;
	layerProgram.shaderId = state.shaderId;
	layerProgram.passes.clear();

	for (const auto& passSource : passSources)
	{
		GLint compileResult = GL_FALSE;
		GLint linkResult = GL_FALSE;
		const char* fragmentSource = passSource.fragmentShaderSource.c_str();

		ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
		glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
		glCompileShader(newVertexShader.get());
		glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
		if (compileResult == GL_FALSE)
		{
			glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
			mRenderer.DestroySingleLayerProgram(layerProgram);
			return false;
		}

		ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
		glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
		glCompileShader(newFragmentShader.get());
		glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
		if (compileResult == GL_FALSE)
		{
			glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
			mRenderer.DestroySingleLayerProgram(layerProgram);
			return false;
		}

		ScopedGlProgram newProgram(glCreateProgram());
		glAttachShader(newProgram.get(), newVertexShader.get());
		glAttachShader(newProgram.get(), newFragmentShader.get());
		glLinkProgram(newProgram.get());
		glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
		if (linkResult == GL_FALSE)
		{
			glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
			mRenderer.DestroySingleLayerProgram(layerProgram);
			return false;
		}

		std::vector<LayerProgram::TextureBinding> textureBindings;
		for (const ShaderTextureAsset& textureAsset : state.textureAssets)
		{
			LayerProgram::TextureBinding textureBinding;
			textureBinding.samplerName = textureAsset.id;
			textureBinding.sourcePath = textureAsset.path;
			if (!mTextureBindings.LoadTextureAsset(textureAsset, textureBinding.texture, loadError))
			{
				for (LayerProgram::TextureBinding& loadedTexture : textureBindings)
				{
					if (loadedTexture.texture != 0)
						glDeleteTextures(1, &loadedTexture.texture);
				}
				CopyErrorMessage(loadError, errorMessageSize, errorMessage);
				mRenderer.DestroySingleLayerProgram(layerProgram);
				return false;
			}
			textureBindings.push_back(textureBinding);
		}

		std::vector<LayerProgram::TextBinding> textBindings;
		mTextureBindings.CreateTextBindings(state, textBindings);

		PassProgram passProgram;
		passProgram.passId = passSource.passId;
		passProgram.inputNames = passSource.inputNames;
		passProgram.outputName = passSource.outputName;
		passProgram.shaderTextureBase = mTextureBindings.ResolveShaderTextureBase(state, mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames());
		passProgram.textureBindings.swap(textureBindings);
		passProgram.textBindings.swap(textBindings);

		const GLuint globalParamsIndex = glGetUniformBlockIndex(newProgram.get(), "GlobalParams");
		if (globalParamsIndex != GL_INVALID_INDEX)
			glUniformBlockBinding(newProgram.get(), globalParamsIndex, kGlobalParamsBindingPoint);

		const unsigned historyCap = mRuntimeSnapshotProvider.GetMaxTemporalHistoryFrames();
		glUseProgram(newProgram.get());
		mTextureBindings.AssignLayerSamplerUniforms(newProgram.get(), state, passProgram, historyCap);
		glUseProgram(0);

		passProgram.program = newProgram.release();
		passProgram.vertexShader = newVertexShader.release();
		passProgram.fragmentShader = newFragmentShader.release();
		layerProgram.passes.push_back(std::move(passProgram));
	}
	return true;
}

bool ShaderProgramCompiler::CompileDecodeShader(int errorMessageSize, char* errorMessage)
{
	GLsizei errorBufferSize = 0;
	GLint compileResult = GL_FALSE;
	GLint linkResult = GL_FALSE;
	const char* vertexSource = kFullscreenTriangleVertexShaderSource;
	const char* fragmentSource = kDecodeFragmentShaderSource;

	ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
	glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
	glCompileShader(newVertexShader.get());
	glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
	if (compileResult == GL_FALSE)
	{
		glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
		return false;
	}

	ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
	glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
	glCompileShader(newFragmentShader.get());
	glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
	if (compileResult == GL_FALSE)
	{
		glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
		return false;
	}

	ScopedGlProgram newProgram(glCreateProgram());
	glAttachShader(newProgram.get(), newVertexShader.get());
	glAttachShader(newProgram.get(), newFragmentShader.get());
	glLinkProgram(newProgram.get());
	glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
	if (linkResult == GL_FALSE)
	{
		glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
		return false;
	}

	mRenderer.DestroyDecodeShaderProgram();
	mRenderer.SetDecodeShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
	return true;
}

bool ShaderProgramCompiler::CompileOutputPackShader(int errorMessageSize, char* errorMessage)
{
	GLsizei errorBufferSize = 0;
	GLint compileResult = GL_FALSE;
	GLint linkResult = GL_FALSE;
	const char* vertexSource = kFullscreenTriangleVertexShaderSource;
	const char* fragmentSource = kOutputPackFragmentShaderSource;

	ScopedGlShader newVertexShader(glCreateShader(GL_VERTEX_SHADER));
	glShaderSource(newVertexShader.get(), 1, (const GLchar**)&vertexSource, NULL);
	glCompileShader(newVertexShader.get());
	glGetShaderiv(newVertexShader.get(), GL_COMPILE_STATUS, &compileResult);
	if (compileResult == GL_FALSE)
	{
		glGetShaderInfoLog(newVertexShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
		return false;
	}

	ScopedGlShader newFragmentShader(glCreateShader(GL_FRAGMENT_SHADER));
	glShaderSource(newFragmentShader.get(), 1, (const GLchar**)&fragmentSource, NULL);
	glCompileShader(newFragmentShader.get());
	glGetShaderiv(newFragmentShader.get(), GL_COMPILE_STATUS, &compileResult);
	if (compileResult == GL_FALSE)
	{
		glGetShaderInfoLog(newFragmentShader.get(), errorMessageSize, &errorBufferSize, errorMessage);
		return false;
	}

	ScopedGlProgram newProgram(glCreateProgram());
	glAttachShader(newProgram.get(), newVertexShader.get());
	glAttachShader(newProgram.get(), newFragmentShader.get());
	glLinkProgram(newProgram.get());
	glGetProgramiv(newProgram.get(), GL_LINK_STATUS, &linkResult);
	if (linkResult == GL_FALSE)
	{
		glGetProgramInfoLog(newProgram.get(), errorMessageSize, &errorBufferSize, errorMessage);
		return false;
	}

	glUseProgram(newProgram.get());
	const GLint outputSamplerLocation = glGetUniformLocation(newProgram.get(), "uOutputRgb");
	if (outputSamplerLocation >= 0)
		glUniform1i(outputSamplerLocation, 0);
	glUseProgram(0);

	mRenderer.DestroyOutputPackShaderProgram();
	mRenderer.SetOutputPackShaderProgram(newProgram.release(), newVertexShader.release(), newFragmentShader.release());
	return true;
}
