#include "stdafx.h"
#include "ShaderCompiler.h"

#include "NativeHandles.h"

#include <fstream>
#include <cctype>
#include <regex>
#include <sstream>
#include <vector>

namespace
{
std::string ReplaceAll(std::string text, const std::string& from, const std::string& to)
{
	std::size_t offset = 0;
	while ((offset = text.find(from, offset)) != std::string::npos)
	{
		text.replace(offset, from.length(), to);
		offset += to.length();
	}
	return text;
}

std::string SlangCBufferTypeForParameter(ShaderParameterType type)
{
	switch (type)
	{
	case ShaderParameterType::Float: return "float";
	case ShaderParameterType::Vec2: return "float2";
	case ShaderParameterType::Color: return "float4";
	case ShaderParameterType::Boolean: return "bool";
	case ShaderParameterType::Enum: return "int";
	case ShaderParameterType::Text: return "";
	case ShaderParameterType::Trigger: return "int";
	}
	return "float";
}

std::string CapitalizeIdentifier(const std::string& identifier)
{
	if (identifier.empty())
		return identifier;
	std::string text = identifier;
	text[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(text[0])));
	return text;
}

std::string BuildParameterUniforms(const std::vector<ShaderParameterDefinition>& parameters)
{
	std::ostringstream source;
	for (const ShaderParameterDefinition& definition : parameters)
	{
		if (definition.type == ShaderParameterType::Text)
		{
			source << "\tfloat " << definition.id << "TextureActiveWidthScale;\n";
			source << "\tfloat " << definition.id << "TextureAspect;\n";
			continue;
		}
		if (definition.type == ShaderParameterType::Trigger)
		{
			source << "\tint " << definition.id << ";\n";
			source << "\tfloat " << definition.id << "Time;\n";
			continue;
		}
		source << "\t" << SlangCBufferTypeForParameter(definition.type) << " " << definition.id << ";\n";
	}
	return source.str();
}

std::string BuildHistorySamplerDeclarations(const std::string& samplerPrefix, unsigned historyLength)
{
	std::ostringstream source;
	for (unsigned index = 0; index < historyLength; ++index)
		source << "Sampler2D<float4> " << samplerPrefix << index << ";\n";
	return source.str();
}

std::string BuildTextureSamplerDeclarations(const std::vector<ShaderTextureAsset>& textureAssets)
{
	std::ostringstream source;
	for (const ShaderTextureAsset& textureAsset : textureAssets)
		source << "Sampler2D<float4> " << textureAsset.id << ";\n";
	if (!textureAssets.empty())
		source << "\n";
	return source.str();
}

std::string BuildTextSamplerDeclarations(const std::vector<ShaderParameterDefinition>& parameters)
{
	std::ostringstream source;
	for (const ShaderParameterDefinition& definition : parameters)
	{
		if (definition.type != ShaderParameterType::Text)
			continue;
		source << "Sampler2D<float4> " << definition.id << "Texture;\n";
	}
	if (source.tellp() > 0)
		source << "\n";
	return source.str();
}

std::string BuildTextHelpers(const std::vector<ShaderParameterDefinition>& parameters)
{
	std::ostringstream source;
	bool emittedMedian = false;
	for (const ShaderParameterDefinition& definition : parameters)
	{
		if (definition.type != ShaderParameterType::Text)
			continue;
		if (!emittedMedian)
		{
			source
				<< "float median(float r, float g, float b)\n"
				<< "{\n"
				<< "\treturn max(min(r, g), min(max(r, g), b));\n"
				<< "}\n\n";
			emittedMedian = true;
		}
		const std::string suffix = CapitalizeIdentifier(definition.id);
		source
			<< "float4 sample" << suffix << "Mtsdf(float2 uv)\n"
			<< "{\n"
			<< "\tif (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)\n"
			<< "\t\treturn float4(0.0, 0.0, 0.0, 0.0);\n"
			<< "\treturn " << definition.id << "Texture.Sample(float2(uv.x * " << definition.id << "TextureActiveWidthScale, uv.y));\n"
			<< "}\n\n"
			<< "float sample" << suffix << "Msdf(float2 uv)\n"
			<< "{\n"
			<< "\tfloat4 mtsdf = sample" << suffix << "Mtsdf(uv);\n"
			<< "\treturn median(mtsdf.r, mtsdf.g, mtsdf.b);\n"
			<< "}\n\n"
			<< "float sample" << suffix << "(float2 uv)\n"
			<< "{\n"
			<< "\treturn sample" << suffix << "Mtsdf(uv).a;\n"
			<< "}\n\n"
			<< "float4 draw" << suffix << "(float2 uv, float4 fillColor)\n"
			<< "{\n"
			<< "\tfloat alpha = sample" << suffix << "(uv) * fillColor.a;\n"
			<< "\treturn float4(fillColor.rgb * alpha, alpha);\n"
			<< "}\n\n";
	}
	return source.str();
}

std::string BuildHistorySwitchCases(const std::string& samplerPrefix, unsigned historyLength)
{
	std::ostringstream source;
	for (unsigned index = 0; index < historyLength; ++index)
		source << "\tcase " << index << ": return " << samplerPrefix << index << ".Sample(tc);\n";
	return source.str();
}
}

ShaderCompiler::ShaderCompiler(
	const std::filesystem::path& repoRoot,
	const std::filesystem::path& wrapperPath,
	const std::filesystem::path& generatedGlslPath,
	const std::filesystem::path& patchedGlslPath,
	unsigned maxTemporalHistoryFrames)
	: mRepoRoot(repoRoot),
	mWrapperPath(wrapperPath),
	mGeneratedGlslPath(generatedGlslPath),
	mPatchedGlslPath(patchedGlslPath),
	mMaxTemporalHistoryFrames(maxTemporalHistoryFrames)
{
}

bool ShaderCompiler::BuildPassFragmentShaderSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& fragmentShaderSource, std::string& error) const
{
	std::string wrapperSource;
	if (!BuildWrapperSlangSource(shaderPackage, pass, wrapperSource, error))
		return false;
	if (!WriteTextFile(mWrapperPath, wrapperSource, error))
		return false;

	if (!RunSlangCompiler(mWrapperPath, mGeneratedGlslPath, error))
		return false;

	fragmentShaderSource = ReadTextFile(mGeneratedGlslPath, error);
	if (fragmentShaderSource.empty())
		return false;

	if (!PatchGeneratedGlsl(fragmentShaderSource, error))
		return false;

	if (!WriteTextFile(mPatchedGlslPath, fragmentShaderSource, error))
		return false;

	return true;
}

bool ShaderCompiler::BuildWrapperSlangSource(const ShaderPackage& shaderPackage, const ShaderPassDefinition& pass, std::string& wrapperSource, std::string& error) const
{
	const std::filesystem::path templatePath = mRepoRoot / "runtime" / "templates" / "shader_wrapper.slang.in";
	wrapperSource = ReadTextFile(templatePath, error);
	if (wrapperSource.empty())
		return false;

	wrapperSource = ReplaceAll(wrapperSource, "{{PARAMETER_UNIFORMS}}", BuildParameterUniforms(shaderPackage.parameters));
	const unsigned historySamplerCount = shaderPackage.temporal.enabled ? mMaxTemporalHistoryFrames : 0;
	wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gSourceHistory", historySamplerCount));
	wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SAMPLERS}}", BuildHistorySamplerDeclarations("gTemporalHistory", historySamplerCount));
	wrapperSource = ReplaceAll(wrapperSource, "{{FEEDBACK_SAMPLER}}", shaderPackage.feedback.enabled ? "Sampler2D<float4> gFeedbackState;\n" : "");
	wrapperSource = ReplaceAll(wrapperSource, "{{FEEDBACK_HELPER}}",
		shaderPackage.feedback.enabled
			? "float4 sampleFeedback(float2 tc)\n{\n\tif (gFeedbackAvailable <= 0)\n\t\treturn float4(0.0, 0.0, 0.0, 0.0);\n\treturn gFeedbackState.Sample(tc);\n}\n"
			: "float4 sampleFeedback(float2 tc)\n{\n\treturn float4(0.0, 0.0, 0.0, 0.0);\n}\n");
	wrapperSource = ReplaceAll(wrapperSource, "{{TEXTURE_SAMPLERS}}", BuildTextureSamplerDeclarations(shaderPackage.textureAssets));
	wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_SAMPLERS}}", BuildTextSamplerDeclarations(shaderPackage.parameters));
	wrapperSource = ReplaceAll(wrapperSource, "{{TEXT_HELPERS}}", BuildTextHelpers(shaderPackage.parameters));
	wrapperSource = ReplaceAll(wrapperSource, "{{SOURCE_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gSourceHistory", historySamplerCount));
	wrapperSource = ReplaceAll(wrapperSource, "{{TEMPORAL_HISTORY_SWITCH_CASES}}", BuildHistorySwitchCases("gTemporalHistory", historySamplerCount));
	wrapperSource = ReplaceAll(wrapperSource, "{{USER_SHADER_INCLUDE}}", pass.sourcePath.generic_string());
	wrapperSource = ReplaceAll(wrapperSource, "{{ENTRY_POINT_CALL}}", pass.entryPoint + "(context)");
	return true;
}

bool ShaderCompiler::FindSlangCompiler(std::filesystem::path& compilerPath, std::string& error) const
{
	char slangRootBuffer[MAX_PATH] = {};
	const DWORD slangRootLength = GetEnvironmentVariableA("SLANG_ROOT", slangRootBuffer, static_cast<DWORD>(sizeof(slangRootBuffer)));
	if (slangRootLength > 0 && slangRootLength < sizeof(slangRootBuffer))
	{
		std::filesystem::path candidate = std::filesystem::path(slangRootBuffer) / "bin" / "slangc.exe";
		if (std::filesystem::exists(candidate))
		{
			compilerPath = candidate;
			return true;
		}
	}

	std::filesystem::path thirdPartyRoot = mRepoRoot / "3rdParty";
	if (!std::filesystem::exists(thirdPartyRoot))
	{
		error = "3rdParty directory was not found under the repository root.";
		return false;
	}

	for (const auto& entry : std::filesystem::directory_iterator(thirdPartyRoot))
	{
		if (!entry.is_directory())
			continue;
		std::filesystem::path candidate = entry.path() / "bin" / "slangc.exe";
		if (std::filesystem::exists(candidate))
		{
			compilerPath = candidate;
			return true;
		}
	}

	error = "Could not find slangc.exe under 3rdParty.";
	return false;
}

bool ShaderCompiler::RunSlangCompiler(const std::filesystem::path& wrapperPath, const std::filesystem::path& outputPath, std::string& error) const
{
	std::filesystem::path compilerPath;
	if (!FindSlangCompiler(compilerPath, error))
		return false;

	std::string commandLine = "\"" + compilerPath.string() + "\" \"" + wrapperPath.string()
		+ "\" -target glsl -profile glsl_430 -entry fragmentMain -stage fragment -o \"" + outputPath.string() + "\"";

	STARTUPINFOA startupInfo = {};
	PROCESS_INFORMATION processInfo = {};
	startupInfo.cb = sizeof(startupInfo);
	std::vector<char> mutableCommandLine(commandLine.begin(), commandLine.end());
	mutableCommandLine.push_back('\0');

	if (!CreateProcessA(NULL, mutableCommandLine.data(), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, mRepoRoot.string().c_str(), &startupInfo, &processInfo))
	{
		error = "Failed to launch slangc.exe.";
		return false;
	}

	UniqueHandle processHandle(processInfo.hProcess);
	UniqueHandle threadHandle(processInfo.hThread);

	WaitForSingleObject(processHandle.get(), INFINITE);

	DWORD exitCode = 0;
	GetExitCodeProcess(processHandle.get(), &exitCode);

	if (exitCode != 0)
	{
		error = "slangc.exe returned a non-zero exit code while compiling the active shader package.";
		return false;
	}

	return true;
}

bool ShaderCompiler::PatchGeneratedGlsl(std::string& shaderText, std::string& error) const
{
	if (shaderText.find("#version 450") == std::string::npos)
	{
		error = "Generated GLSL did not include the expected version header.";
		return false;
	}

	shaderText = ReplaceAll(shaderText, "#version 450", "#version 430 core");
	shaderText = std::regex_replace(shaderText, std::regex(R"(#extension GL_EXT_samplerless_texture_functions : require\r?\n)"), "");
	shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) uniform;\r?\n)"), "");
	shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(row_major\) buffer;\r?\n)"), "");
	shaderText = std::regex_replace(shaderText, std::regex(R"(layout\(location = 0\)\s*in vec2 ([A-Za-z0-9_]+);)"), "in vec2 vTexCoord;");
	shaderText = ReplaceAll(shaderText, "input_texCoord_0", "vTexCoord");

	std::smatch match;
	std::regex outRegex(R"(layout\(location = 0\)\s*out vec4 ([A-Za-z0-9_]+);)");
	if (std::regex_search(shaderText, match, outRegex))
	{
		const std::string outputName = match[1].str();
		shaderText = std::regex_replace(shaderText, outRegex, "layout(location = 0) out vec4 fragColor;");
		shaderText = ReplaceAll(shaderText, outputName + " =", "fragColor =");
	}

	return true;
}

std::string ShaderCompiler::ReadTextFile(const std::filesystem::path& path, std::string& error) const
{
	std::ifstream input(path, std::ios::binary);
	if (!input)
	{
		error = "Could not open file: " + path.string();
		return std::string();
	}

	std::ostringstream buffer;
	buffer << input.rdbuf();
	return buffer.str();
}

bool ShaderCompiler::WriteTextFile(const std::filesystem::path& path, const std::string& contents, std::string& error) const
{
	std::error_code fsError;
	std::filesystem::create_directories(path.parent_path(), fsError);

	std::ofstream output(path, std::ios::binary);
	if (!output)
	{
		error = "Could not write file: " + path.string();
		return false;
	}

	output << contents;
	return output.good();
}
