#pragma once

#include "AppConfig.h"
#include "AppConfigProvider.h"
#include "../logging/Logger.h"
#include "../runtime/RuntimeShaderBridge.h"
#include "../runtime/SupportedShaderCatalog.h"
#include "../control/RuntimeStateJson.h"
#include "../telemetry/TelemetryHealthMonitor.h"
#include "../video/DeckLinkOutput.h"
#include "../video/DeckLinkOutputThread.h"

#include <chrono>
#include <filesystem>
#include <mutex>
#include <string>
#include <thread>
#include <type_traits>

namespace RenderCadenceCompositor
{
namespace detail
{
template <typename RenderThread>
auto StartRenderThread(RenderThread& renderThread, std::string& error, int) -> decltype(renderThread.Start(error), bool())
{
	return renderThread.Start(error);
}

template <typename RenderThread>
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::true_type)
{
	return renderThread.Start();
}

template <typename RenderThread>
bool StartRenderThreadWithoutError(RenderThread& renderThread, std::false_type)
{
	renderThread.Start();
	return true;
}

template <typename RenderThread>
auto StartRenderThread(RenderThread& renderThread, std::string&, long) -> decltype(renderThread.Start(), bool())
{
	return StartRenderThreadWithoutError(renderThread, std::is_same<decltype(renderThread.Start()), bool>());
}
}

template <typename RenderThread, typename SystemFrameExchange>
class RenderCadenceApp
{
public:
	RenderCadenceApp(RenderThread& renderThread, SystemFrameExchange& frameExchange, AppConfig config = DefaultAppConfig()) :
		mRenderThread(renderThread),
		mFrameExchange(frameExchange),
		mConfig(config),
		mOutputThread(mOutput, mFrameExchange, mConfig.outputThread),
		mTelemetryHealth(mConfig.telemetry)
	{
	}

	RenderCadenceApp(const RenderCadenceApp&) = delete;
	RenderCadenceApp& operator=(const RenderCadenceApp&) = delete;

	~RenderCadenceApp()
	{
		Stop();
	}

	bool Start(std::string& error)
	{
		LoadSupportedShaderCatalog();

		Log("app", "Starting render thread.");
		if (!detail::StartRenderThread(mRenderThread, error, 0))
		{
			LogError("app", "Render thread start failed: " + error);
			Stop();
			return false;
		}
		StartRuntimeShaderBuild();

		Log("app", "Waiting for rendered warmup frames.");
		if (!mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, mConfig.warmupTimeout))
		{
			error = "Timed out waiting for rendered warmup frames.";
			LogError("app", error);
			Stop();
			return false;
		}

		StartOptionalVideoOutput();
		mTelemetryHealth.Start(mFrameExchange, mOutput, mOutputThread, mRenderThread);
		StartHttpServer();
		Log("app", "RenderCadenceCompositor started.");
		mStarted = true;
		return true;
	}

	void Stop()
	{
		mHttpServer.Stop();
		mTelemetryHealth.Stop();
		mOutputThread.Stop();
		mOutput.Stop();
		StopRuntimeShaderBuild();
		mRenderThread.Stop();
		mOutput.ReleaseResources();
		if (mStarted)
			Log("app", "RenderCadenceCompositor shutdown complete.");
		mStarted = false;
	}

	bool Started() const { return mStarted; }
	const DeckLinkOutput& Output() const { return mOutput; }

private:
	void StartOptionalVideoOutput()
	{
		std::string outputError;
		Log("app", "Initializing optional DeckLink output.");
		if (!mOutput.Initialize(
			mConfig.deckLink,
			[this](const VideoIOCompletion& completion) {
				mFrameExchange.ReleaseScheduledByBytes(completion.outputFrameBuffer);
			},
			outputError))
		{
			DisableVideoOutput("DeckLink output unavailable: " + outputError);
			return;
		}

		Log("app", "Starting DeckLink output thread.");
		if (!mOutputThread.Start())
		{
			DisableVideoOutput("DeckLink output thread failed to start.");
			return;
		}

		Log("app", "Waiting for DeckLink preroll frames.");
		if (!WaitForPreroll())
		{
			DisableVideoOutput("Timed out waiting for DeckLink preroll frames.");
			return;
		}

		Log("app", "Starting DeckLink scheduled playback.");
		if (!mOutput.StartScheduledPlayback(outputError))
		{
			DisableVideoOutput("DeckLink scheduled playback failed: " + outputError);
			return;
		}

		mVideoOutputEnabled = true;
		mVideoOutputStatus = "DeckLink scheduled output running.";
		Log("app", mVideoOutputStatus);
	}

	void DisableVideoOutput(const std::string& reason)
	{
		mOutputThread.Stop();
		mOutput.Stop();
		mOutput.ReleaseResources();
		mFrameExchange.Clear();
		mVideoOutputEnabled = false;
		mVideoOutputStatus = reason;
		LogWarning("app", reason + " Continuing without video output.");
	}

	void StartHttpServer()
	{
		HttpControlServerCallbacks callbacks;
		callbacks.getStateJson = [this]() {
			return BuildStateJson();
		};

		std::string error;
		if (!mHttpServer.Start(
			FindRepoPath("ui/dist"),
			FindRepoPath("docs"),
			mConfig.http,
			callbacks,
			error))
		{
			LogWarning("http", "HTTP control server did not start: " + error);
			return;
		}
	}

	std::string BuildStateJson()
	{
		CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
		RuntimeDisplayState runtimeState = CopyRuntimeDisplayState(telemetry);
		return RuntimeStateToJson(RuntimeStateJsonInput{
			mConfig,
			telemetry,
			mHttpServer.Port(),
			mVideoOutputEnabled,
			mVideoOutputStatus,
			mShaderCatalog.Shaders(),
			runtimeState.compileSucceeded,
			runtimeState.compileMessage,
			runtimeState.activeShaderPackage
		});
	}

	bool WaitForPreroll() const
	{
		const auto deadline = std::chrono::steady_clock::now() + mConfig.prerollTimeout;
		while (std::chrono::steady_clock::now() < deadline)
		{
			if (mFrameExchange.Metrics().scheduledCount >= mConfig.outputThread.targetBufferedFrames)
				return true;
			std::this_thread::sleep_for(mConfig.prerollPoll);
		}
		return false;
	}

	void StartRuntimeShaderBuild()
	{
		if (mConfig.runtimeShaderId.empty())
		{
			Log("runtime-shader", "Runtime shader build disabled.");
			return;
		}

		Log("runtime-shader", "Starting background Slang build for shader '" + mConfig.runtimeShaderId + "'.");
		SetRuntimeDisplayState(true, "Runtime Slang build started for shader '" + mConfig.runtimeShaderId + "'.", mConfig.runtimeShaderId);
		mShaderBridge.Start(
			mConfig.runtimeShaderId,
			[this](const RuntimeShaderArtifact& artifact) {
				SetRuntimeDisplayState(true, artifact.message.empty() ? "Runtime shader artifact is ready." : artifact.message, artifact.shaderId);
				mRenderThread.SubmitRuntimeShaderArtifact(artifact);
			},
			[this](const std::string& message) {
				SetRuntimeDisplayState(false, message);
				LogError("runtime-shader", "Runtime Slang build failed: " + message);
			});
	}

	void LoadSupportedShaderCatalog()
	{
		const std::filesystem::path shaderRoot = FindRepoPath(mConfig.shaderLibrary);
		std::string error;
		if (!mShaderCatalog.Load(shaderRoot, static_cast<unsigned>(mConfig.maxTemporalHistoryFrames), error))
		{
			LogWarning("runtime-shader", "Supported shader catalog is empty: " + error);
			return;
		}

		Log("runtime-shader", "Supported shader catalog loaded with " + std::to_string(mShaderCatalog.Shaders().size()) + " shader(s).");
	}

	void StopRuntimeShaderBuild()
	{
		mShaderBridge.Stop();
	}

	struct RuntimeDisplayState
	{
		bool compileSucceeded = true;
		std::string compileMessage;
		const ShaderPackage* activeShaderPackage = nullptr;
	};

	void SetRuntimeDisplayState(bool compileSucceeded, const std::string& compileMessage, const std::string& activeShaderId = std::string())
	{
		std::lock_guard<std::mutex> lock(mRuntimeDisplayMutex);
		mRuntimeCompileSucceeded = compileSucceeded;
		mRuntimeCompileMessage = compileMessage;
		if (!activeShaderId.empty())
			mActiveShaderId = activeShaderId;
	}

	RuntimeDisplayState CopyRuntimeDisplayState(const CadenceTelemetrySnapshot& telemetry) const
	{
		std::lock_guard<std::mutex> lock(mRuntimeDisplayMutex);
		RuntimeDisplayState state;
		state.compileSucceeded = mRuntimeCompileSucceeded && telemetry.shaderBuildFailures == 0;
		state.compileMessage = mRuntimeCompileMessage;
		if (telemetry.shaderBuildFailures > 0)
			state.compileMessage = "Runtime shader GL commit failed; see logs for details.";
		if (state.compileMessage.empty())
			state.compileMessage = mConfig.runtimeShaderId.empty()
				? "Runtime shader build disabled."
				: "Runtime shader build has not completed yet.";
		state.activeShaderPackage = mShaderCatalog.FindPackage(mActiveShaderId);
		return state;
	}

	RenderThread& mRenderThread;
	SystemFrameExchange& mFrameExchange;
	AppConfig mConfig;
	DeckLinkOutput mOutput;
	DeckLinkOutputThread<SystemFrameExchange> mOutputThread;
	TelemetryHealthMonitor mTelemetryHealth;
	CadenceTelemetry mHttpTelemetry;
	HttpControlServer mHttpServer;
	RuntimeShaderBridge mShaderBridge;
	SupportedShaderCatalog mShaderCatalog;
	mutable std::mutex mRuntimeDisplayMutex;
	bool mRuntimeCompileSucceeded = true;
	std::string mRuntimeCompileMessage;
	std::string mActiveShaderId;
	bool mStarted = false;
	bool mVideoOutputEnabled = false;
	std::string mVideoOutputStatus = "DeckLink output not started.";
};
}
