#pragma once

#include "AppConfig.h"
#include "AppConfigProvider.h"
#include "RuntimeLayerController.h"
#include "../logging/Logger.h"
#include "../control/RuntimeStateJson.h"
#include "../preview/PreviewWindowThread.h"
#include "../telemetry/TelemetryHealthMonitor.h"
#include "DeckLinkOutput.h"
#include "VideoIOEdges.h"
#include "VideoOutputThread.h"

#include <chrono>
#include <filesystem>
#include <functional>
#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, VideoOutputThreadConfig{
			mConfig.outputThread.targetBufferedFrames,
			mConfig.outputThread.idleSleep
		}),
		mTelemetryHealth(mConfig.telemetry),
		mRuntimeLayers([this](const std::vector<RuntimeRenderLayerModel>& layers) {
			mRenderThread.SubmitRuntimeRenderLayers(layers);
		})
	{
	}

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

	~RenderCadenceApp()
	{
		Stop();
	}

	bool Start(std::string& error)
	{
		mRuntimeLayers.Initialize(
			mConfig.shaderLibrary,
			static_cast<unsigned>(mConfig.maxTemporalHistoryFrames),
			mConfig.runtimeShaderId);

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

		if (!BuildSettledOutputReserve(error))
		{
			LogError("app", error);
			Stop();
			return false;
		}

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

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

	bool Started() const { return mStarted; }
	const IVideoOutputEdge& Output() const { return mOutput; }
	void SetVideoInputMetricsProvider(std::function<VideoInputEdgeMetrics()> provider)
	{
		mVideoInputMetricsProvider = std::move(provider);
	}

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 video output thread.");
		if (!mOutputThread.Start())
		{
			DisableVideoOutput("Video 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);
		Log(
			"app",
			"DeckLink output mode: " + mOutput.State().outputDisplayModeName +
				", frame budget " + std::to_string(mOutput.State().frameBudgetMilliseconds) + " ms.");
	}

	bool BuildSettledOutputReserve(std::string& error)
	{
		const auto reserveTimeout = mConfig.warmupTimeout;
		Log("app",
			"Building output preroll reserve: waiting for " + std::to_string(mConfig.warmupCompletedFrames) +
			" completed frame(s).");
		if (mFrameExchange.WaitForCompletedDepth(mConfig.warmupCompletedFrames, reserveTimeout))
		{
			return true;
		}

		error = "Timed out waiting for output preroll reserve.";
		return false;
	}

	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();
		};
		callbacks.addLayer = [this](const std::string& body) {
			return mRuntimeLayers.HandleAddLayer(body);
		};
		callbacks.removeLayer = [this](const std::string& body) {
			return mRuntimeLayers.HandleRemoveLayer(body);
		};
		callbacks.executePost = [this](const std::string& path, const std::string& body) {
			RuntimeControlCommand command;
			std::string error;
			if (!ParseRuntimeControlCommand(path, body, command, error))
				return ControlActionResult{ false, error };
			return mRuntimeLayers.HandleControlCommand(command);
		};

		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;
		}
	}

	void StartPreviewWindow()
	{
		if (!mConfig.previewEnabled)
			return;

		PreviewWindowConfig previewConfig;
		previewConfig.enabled = true;
		previewConfig.fps = mConfig.previewFps;
		std::string error;
		if (mPreviewWindow.Start(mFrameExchange, previewConfig, error))
		{
			Log("preview", "Preview window thread started.");
			return;
		}

		LogWarning("preview", "Preview window did not start: " + error);
	}

	std::string BuildStateJson()
	{
		CadenceTelemetrySnapshot telemetry = mHttpTelemetry.Sample(mFrameExchange, mOutput, mOutputThread, mRenderThread);
		ApplyVideoInputMetrics(telemetry);
		RuntimeLayerModelSnapshot layerSnapshot = mRuntimeLayers.Snapshot(telemetry);
		return RuntimeStateToJson(RuntimeStateJsonInput{
			mConfig,
			telemetry,
			mHttpServer.Port(),
			mVideoOutputEnabled,
			mVideoOutputStatus,
			mRuntimeLayers.ShaderCatalog(),
			layerSnapshot
		});
	}

	void ApplyVideoInputMetrics(CadenceTelemetrySnapshot& telemetry)
	{
		if (!mVideoInputMetricsProvider)
			return;

		const VideoInputEdgeMetrics inputMetrics = mVideoInputMetricsProvider();
		telemetry.inputConvertMilliseconds = inputMetrics.convertMilliseconds;
		telemetry.inputSubmitMilliseconds = inputMetrics.submitMilliseconds;
		telemetry.inputNoSignalFrames = inputMetrics.noInputSourceFrames;
		telemetry.inputUnsupportedFrames = inputMetrics.unsupportedFrames;
		telemetry.inputSubmitMisses = inputMetrics.submitMisses;
		telemetry.inputCaptureFormat = inputMetrics.captureFormat ? inputMetrics.captureFormat : "none";
		if (telemetry.sampleSeconds > 0.0)
			telemetry.inputCaptureFps = static_cast<double>(inputMetrics.capturedFrames - mLastInputCapturedFrames) / telemetry.sampleSeconds;
		mLastInputCapturedFrames = inputMetrics.capturedFrames;
	}

	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;
	}

	RenderThread& mRenderThread;
	SystemFrameExchange& mFrameExchange;
	AppConfig mConfig;
	DeckLinkOutput mOutput;
	VideoOutputThread<SystemFrameExchange> mOutputThread;
	TelemetryHealthMonitor mTelemetryHealth;
	CadenceTelemetry mHttpTelemetry;
	HttpControlServer mHttpServer;
	PreviewWindowThread mPreviewWindow;
	RuntimeLayerController mRuntimeLayers;
	std::function<VideoInputEdgeMetrics()> mVideoInputMetricsProvider;
	uint64_t mLastInputCapturedFrames = 0;
	bool mStarted = false;
	bool mVideoOutputEnabled = false;
	std::string mVideoOutputStatus = "DeckLink output not started.";
};
}
