#include "ShaderBuildQueue.h"

#include "RuntimeEventDispatcher.h"

#include <chrono>
#include <utility>

namespace
{
constexpr auto kShaderBuildDebounce = std::chrono::milliseconds(400);
}

ShaderBuildQueue::ShaderBuildQueue(RuntimeSnapshotProvider& runtimeSnapshotProvider, RuntimeEventDispatcher& runtimeEventDispatcher) :
	mRuntimeSnapshotProvider(runtimeSnapshotProvider),
	mRuntimeEventDispatcher(runtimeEventDispatcher),
	mWorkerThread([this]() { WorkerLoop(); })
{
}

ShaderBuildQueue::~ShaderBuildQueue()
{
	Stop();
}

void ShaderBuildQueue::RequestBuild(unsigned outputWidth, unsigned outputHeight)
{
	{
		std::lock_guard<std::mutex> lock(mMutex);
		mHasRequest = true;
		++mRequestedGeneration;
		mRequestedOutputWidth = outputWidth;
		mRequestedOutputHeight = outputHeight;
		mHasReadyBuild = false;
	}
	mCondition.notify_one();
}

bool ShaderBuildQueue::TryConsumeReadyBuild(PreparedShaderBuild& build)
{
	std::lock_guard<std::mutex> lock(mMutex);
	if (!mHasReadyBuild)
		return false;

	build = std::move(mReadyBuild);
	mReadyBuild = PreparedShaderBuild();
	mHasReadyBuild = false;
	return true;
}

bool ShaderBuildQueue::TryConsumeReadyBuild(uint64_t expectedGeneration, PreparedShaderBuild& build)
{
	std::lock_guard<std::mutex> lock(mMutex);
	if (!mHasReadyBuild || mReadyBuild.generation != expectedGeneration)
		return false;

	build = std::move(mReadyBuild);
	mReadyBuild = PreparedShaderBuild();
	mHasReadyBuild = false;
	return true;
}

void ShaderBuildQueue::Stop()
{
	{
		std::lock_guard<std::mutex> lock(mMutex);
		if (mStopping)
			return;
		mStopping = true;
	}
	mCondition.notify_one();
	if (mWorkerThread.joinable())
		mWorkerThread.join();
}

void ShaderBuildQueue::WorkerLoop()
{
	for (;;)
	{
		uint64_t generation = 0;
		unsigned outputWidth = 0;
		unsigned outputHeight = 0;
		{
			std::unique_lock<std::mutex> lock(mMutex);
			mCondition.wait(lock, [this]() { return mStopping || mHasRequest; });
			if (mStopping)
				return;

			generation = mRequestedGeneration;
			outputWidth = mRequestedOutputWidth;
			outputHeight = mRequestedOutputHeight;
			mHasRequest = false;
		}

		for (;;)
		{
			std::unique_lock<std::mutex> lock(mMutex);
			if (mCondition.wait_for(lock, kShaderBuildDebounce, [this, generation]() {
				return mStopping || (mHasRequest && mRequestedGeneration != generation);
			}))
			{
				if (mStopping)
					return;

				generation = mRequestedGeneration;
				outputWidth = mRequestedOutputWidth;
				outputHeight = mRequestedOutputHeight;
				mHasRequest = false;
				continue;
			}
			break;
		}

		PreparedShaderBuild build = Build(generation, outputWidth, outputHeight);

		bool shouldPublish = false;
		{
			std::lock_guard<std::mutex> lock(mMutex);
			if (mStopping)
				return;
			if (generation != mRequestedGeneration)
				continue;
			mReadyBuild = build;
			mHasReadyBuild = true;
			shouldPublish = true;
		}

		if (shouldPublish)
			PublishBuildLifecycleEvent(build, outputWidth, outputHeight);
	}
}

PreparedShaderBuild ShaderBuildQueue::Build(uint64_t generation, unsigned outputWidth, unsigned outputHeight)
{
	PreparedShaderBuild build;
	build.generation = generation;
	build.renderSnapshot = mRuntimeSnapshotProvider.PublishRenderStateSnapshot(outputWidth, outputHeight);
	build.layers.reserve(build.renderSnapshot.states.size());

	for (const RuntimeRenderState& state : build.renderSnapshot.states)
	{
		PreparedLayerShader layer;
		layer.state = state;
		if (!mRuntimeSnapshotProvider.BuildLayerPassFragmentShaderSources(state.layerId, layer.passes, build.message))
		{
			build.succeeded = false;
			return build;
		}
		build.layers.push_back(std::move(layer));
	}

	build.succeeded = true;
	build.message = "Shader layers prepared successfully.";
	return build;
}

void ShaderBuildQueue::PublishBuildLifecycleEvent(const PreparedShaderBuild& build, unsigned outputWidth, unsigned outputHeight) const
{
	ShaderBuildEvent event;
	event.phase = build.succeeded ? RuntimeEventShaderBuildPhase::Prepared : RuntimeEventShaderBuildPhase::Failed;
	event.generation = build.generation;
	event.inputWidth = outputWidth;
	event.inputHeight = outputHeight;
	event.succeeded = build.succeeded;
	event.message = build.message;
	mRuntimeEventDispatcher.PublishPayload(event, "ShaderBuildQueue");
}
