#include "app/AppConfig.h"
#include "app/AppConfigProvider.h"
#include "app/RenderCadenceApp.h"
#include "frames/InputFrameMailbox.h"
#include "frames/SystemFrameExchange.h"
#include "logging/Logger.h"
#include "render/thread/RenderThread.h"
#include "video/decklink/DeckLinkInput.h"
#include "video/decklink/DeckLinkInputThread.h"
#include "video/decklink/DeckLinkDisplayMode.h"
#include "video/core/VideoIOFormat.h"
#include "video/core/VideoMode.h"

#include <windows.h>

#include <chrono>
#include <iostream>
#include <sstream>
#include <string>
#include <thread>

namespace
{
constexpr std::size_t kDeckLinkTargetBufferedFrames = 4;
constexpr std::size_t kReadbackDepth = 6;
constexpr std::size_t kWritableOutputReserveFrames = kReadbackDepth + 2;

class ComInitGuard
{
public:
	~ComInitGuard()
	{
		if (mInitialized)
			CoUninitialize();
	}

	bool Initialize()
	{
		const HRESULT result = CoInitialize(nullptr);
		mInitialized = SUCCEEDED(result);
		mResult = result;
		return mInitialized;
	}

	HRESULT Result() const { return mResult; }

private:
	bool mInitialized = false;
	HRESULT mResult = S_OK;
};

bool WaitForInputWarmup(InputFrameMailbox& mailbox, std::size_t targetReadyFrames, std::chrono::milliseconds timeout)
{
	const auto start = std::chrono::steady_clock::now();
	while (std::chrono::steady_clock::now() - start < timeout)
	{
		const InputFrameMailboxMetrics metrics = mailbox.Metrics();
		if (metrics.readyCount >= targetReadyFrames)
			return true;
		std::this_thread::sleep_for(std::chrono::milliseconds(2));
	}
	return false;
}
}

int main(int argc, char** argv)
{
	RenderCadenceCompositor::AppConfigProvider configProvider;
	std::string configError;
	if (!configProvider.LoadDefault(configError))
	{
		RenderCadenceCompositor::Logger::Instance().Start(RenderCadenceCompositor::DefaultAppConfig().logging);
		RenderCadenceCompositor::LogError("app", "Config load failed: " + configError);
		RenderCadenceCompositor::Logger::Instance().Stop();
		return 1;
	}
	configProvider.ApplyCommandLine(argc, argv);

	RenderCadenceCompositor::AppConfig appConfig = configProvider.Config();
	RenderCadenceCompositor::Logger::Instance().Start(appConfig.logging);
	RenderCadenceCompositor::Log(
		"app",
		"RenderCadenceCompositor starting. Starts render cadence, system-memory exchange, DeckLink scheduled output, and telemetry. Press Enter to stop.");
	RenderCadenceCompositor::Log("app", "Loaded config from " + configProvider.SourcePath().string());

	ComInitGuard com;
	if (!com.Initialize())
	{
		std::ostringstream message;
		message << "COM initialization failed: 0x" << std::hex << com.Result();
		RenderCadenceCompositor::LogError("app", message.str());
		RenderCadenceCompositor::Logger::Instance().Stop();
		return 1;
	}

	SystemFrameExchangeConfig frameExchangeConfig;
	VideoFormatDimensions(
		appConfig.outputVideoFormat,
		frameExchangeConfig.width,
		frameExchangeConfig.height);
	frameExchangeConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
	frameExchangeConfig.rowBytes = VideoIORowBytes(frameExchangeConfig.pixelFormat, frameExchangeConfig.width);
	frameExchangeConfig.capacity =
		appConfig.warmupCompletedFrames +
		kDeckLinkTargetBufferedFrames +
		kWritableOutputReserveFrames;
	frameExchangeConfig.maxCompletedFrames = appConfig.warmupCompletedFrames;

	SystemFrameExchange frameExchange(frameExchangeConfig);

	InputFrameMailboxConfig inputMailboxConfig;
	VideoFormatDimensions(
		appConfig.inputVideoFormat,
		inputMailboxConfig.width,
		inputMailboxConfig.height);
	inputMailboxConfig.pixelFormat = VideoIOPixelFormat::Bgra8;
	inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
	inputMailboxConfig.capacity = 4;
	inputMailboxConfig.maxReadyFrames = 3;
	InputFrameMailbox inputMailbox(inputMailboxConfig);

	VideoFormat inputVideoMode;
	VideoFormat outputVideoMode;
	std::string inputVideoModeError;
	const bool inputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.inputVideoFormat, appConfig.inputFrameRate, inputVideoMode);
	const bool outputVideoModeResolved = ResolveConfiguredVideoFormat(appConfig.outputVideoFormat, appConfig.outputFrameRate, outputVideoMode);
	if (!inputVideoModeResolved)
	{
		inputVideoModeError = "Unsupported DeckLink inputVideoFormat/inputFrameRate in config/runtime-host.json: " +
			appConfig.inputVideoFormat + " / " + appConfig.inputFrameRate;
		RenderCadenceCompositor::LogWarning("app", inputVideoModeError);
	}
	if (!outputVideoModeResolved)
	{
		RenderCadenceCompositor::LogWarning(
			"app",
			"Unsupported DeckLink outputVideoFormat/outputFrameRate in config/runtime-host.json; render cadence will use parsed frame-rate fallback: " +
				appConfig.outputVideoFormat + " / " + appConfig.outputFrameRate);
	}
	else
	{
		appConfig.deckLink.outputVideoMode = outputVideoMode;
	}

	RenderCadenceCompositor::DeckLinkInput deckLinkInput(inputMailbox);
	RenderCadenceCompositor::DeckLinkInputThread deckLinkInputThread(deckLinkInput);
	bool deckLinkInputStarted = false;
	if (inputVideoModeResolved)
	{
		RenderCadenceCompositor::DeckLinkInputConfig deckLinkInputConfig;
		deckLinkInputConfig.videoFormat = inputVideoMode;
		std::string deckLinkInputError;
		if (deckLinkInput.Initialize(deckLinkInputConfig, deckLinkInputError))
		{
			inputMailboxConfig.pixelFormat = deckLinkInput.CapturePixelFormat();
			inputMailboxConfig.rowBytes = VideoIORowBytes(inputMailboxConfig.pixelFormat, inputMailboxConfig.width);
			inputMailbox.Configure(inputMailboxConfig);
		}

		if (deckLinkInput.IsInitialized() && deckLinkInputThread.Start(deckLinkInputError))
		{
			deckLinkInputStarted = true;
			RenderCadenceCompositor::Log("app", "DeckLink input edge started for " + inputVideoMode.displayName + ".");
			RenderCadenceCompositor::Log("app", "Waiting for DeckLink input warmup frames.");
			constexpr std::size_t kInputStartupBufferedFrames = 3;
			constexpr std::chrono::milliseconds kInputWarmupTimeout(1000);
			if (WaitForInputWarmup(inputMailbox, kInputStartupBufferedFrames, kInputWarmupTimeout))
			{
				const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
				RenderCadenceCompositor::Log(
					"app",
					"DeckLink input warmup complete. ready=" + std::to_string(metrics.readyCount) +
						" submitted=" + std::to_string(metrics.submittedFrames) + ".");
			}
			else
			{
				const InputFrameMailboxMetrics metrics = inputMailbox.Metrics();
				RenderCadenceCompositor::LogWarning(
					"app",
					"DeckLink input warmup timed out; starting render cadence with current input buffer. ready=" +
						std::to_string(metrics.readyCount) + " submitted=" + std::to_string(metrics.submittedFrames) + ".");
			}
		}
		else
		{
			RenderCadenceCompositor::LogWarning("app", "DeckLink input edge unavailable; runtime shaders will use fallback input until a real input edge is available. " + deckLinkInputError);
			deckLinkInput.ReleaseResources();
		}
	}
	else
	{
		RenderCadenceCompositor::LogWarning("app", "DeckLink input mode was not resolved; runtime shaders will use fallback input until a real input edge is available.");
	}

	RenderThread::Config renderConfig;
	renderConfig.width = frameExchangeConfig.width;
	renderConfig.height = frameExchangeConfig.height;
	const double fallbackFrameDurationMilliseconds = FrameDurationMillisecondsFromRateString(appConfig.outputFrameRate);
	renderConfig.frameDurationMilliseconds = outputVideoModeResolved
		? outputVideoMode.frameDurationMilliseconds
		: fallbackFrameDurationMilliseconds;
	renderConfig.pboDepth = kReadbackDepth;

	RenderThread renderThread(frameExchange, &inputMailbox, renderConfig);

	RenderCadenceCompositor::RenderCadenceApp<RenderThread, SystemFrameExchange> app(renderThread, frameExchange, appConfig);
	app.SetVideoInputMetricsProvider([&deckLinkInput]() {
		return deckLinkInput.Metrics();
	});

	std::string error;
	if (!app.Start(error))
	{
		RenderCadenceCompositor::LogError("app", "RenderCadenceCompositor start failed: " + error);
		if (deckLinkInputStarted)
			deckLinkInputThread.Stop();
		RenderCadenceCompositor::Logger::Instance().Stop();
		return 1;
	}

	std::string line;
	std::getline(std::cin, line);
	app.Stop();
	if (deckLinkInputStarted)
		deckLinkInputThread.Stop();
	RenderCadenceCompositor::Log("app", "RenderCadenceCompositor stopped.");
	RenderCadenceCompositor::Logger::Instance().Stop();
	return 0;
}
