#include "DeckLinkDisplayMode.h"
#include "OpenGLComposite.h"
#include "GLExtensions.h"
#include "PngScreenshotWriter.h"
#include "RenderEngine.h"
#include "RuntimeCoordinator.h"
#include "RuntimeEventDispatcher.h"
#include "RuntimeServiceLiveBridge.h"
#include "RuntimeServices.h"
#include "RuntimeSnapshotProvider.h"
#include "RuntimeStore.h"
#include "RuntimeUpdateController.h"
#include "ShaderBuildQueue.h"
#include "VideoBackend.h"

#include <chrono>
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <memory>
#include <sstream>
#include <string>
#include <vector>

OpenGLComposite::OpenGLComposite(HWND hWnd, HDC hDC, HGLRC hRC) :
	hGLWnd(hWnd), hGLDC(hDC), hGLRC(hRC)
{
	mRuntimeStore = std::make_unique<RuntimeStore>();
	mRuntimeEventDispatcher = std::make_unique<RuntimeEventDispatcher>();
	mRuntimeSnapshotProvider = std::make_unique<RuntimeSnapshotProvider>(mRuntimeStore->GetRenderSnapshotBuilder(), *mRuntimeEventDispatcher);
	mRuntimeCoordinator = std::make_unique<RuntimeCoordinator>(*mRuntimeStore, *mRuntimeEventDispatcher);
	mRenderEngine = std::make_unique<RenderEngine>(
		*mRuntimeSnapshotProvider,
		mRuntimeStore->GetHealthTelemetry(),
		hGLDC,
		hGLRC,
		[this]() { renderEffect(); },
		[]() {},
		[this]() { paintGL(false); });
	mVideoBackend = std::make_unique<VideoBackend>(*mRenderEngine, mRuntimeStore->GetHealthTelemetry(), *mRuntimeEventDispatcher);
	mShaderBuildQueue = std::make_unique<ShaderBuildQueue>(*mRuntimeSnapshotProvider, *mRuntimeEventDispatcher);
	mRuntimeServices = std::make_unique<RuntimeServices>(*mRuntimeEventDispatcher);
	mRuntimeUpdateController = std::make_unique<RuntimeUpdateController>(
		*mRuntimeStore,
		*mRuntimeCoordinator,
		*mRuntimeEventDispatcher,
		*mRuntimeServices,
		*mRenderEngine,
		*mShaderBuildQueue,
		*mVideoBackend);
}

OpenGLComposite::~OpenGLComposite()
{
	if (mRuntimeServices)
		mRuntimeServices->Stop();
	if (mShaderBuildQueue)
		mShaderBuildQueue->Stop();
	if (mVideoBackend)
		mVideoBackend->ReleaseResources();
	if (mRuntimeStore)
	{
		std::string persistenceError;
		if (!mRuntimeStore->FlushPersistenceForShutdown(std::chrono::seconds(2), persistenceError))
			OutputDebugStringA((std::string("Persistence shutdown flush failed: ") + persistenceError + "\n").c_str());
	}
}

bool OpenGLComposite::InitDeckLink()
{
	return InitVideoIO();
}

bool OpenGLComposite::InitVideoIO()
{
	VideoFormatSelection		videoModes;
	std::string						initFailureReason;

	if (mRuntimeStore && mRuntimeStore->GetRuntimeRepositoryRoot().empty())
	{
		std::string runtimeError;
		if (!mRuntimeStore->InitializeStore(runtimeError))
		{
			MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
			return false;
		}
	}

	if (mRuntimeStore)
	{
		if (!ResolveConfiguredVideoFormats(
			mRuntimeStore->GetConfiguredInputVideoFormat(),
			mRuntimeStore->GetConfiguredInputFrameRate(),
			mRuntimeStore->GetConfiguredOutputVideoFormat(),
			mRuntimeStore->GetConfiguredOutputFrameRate(),
			videoModes,
			initFailureReason))
		{
			MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink mode configuration error", MB_OK);
			return false;
		}
	}

	if (!mVideoBackend->DiscoverDevicesAndModes(videoModes, initFailureReason))
	{
		const char* title = initFailureReason == "Please install the Blackmagic DeckLink drivers to use the features of this application."
			? "This application requires the DeckLink drivers installed."
			: "DeckLink initialization failed";
		MessageBoxA(NULL, initFailureReason.c_str(), title, MB_OK | MB_ICONERROR);
		return false;
	}
	const bool outputAlphaRequired = mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured();
	if (!mVideoBackend->SelectPreferredFormats(videoModes, outputAlphaRequired, initFailureReason))
		goto error;

	if (! CheckOpenGLExtensions())
	{
		initFailureReason = "OpenGL extension checks failed.";
		goto error;
	}

	if (! InitOpenGLState())
	{
		initFailureReason = "OpenGL state initialization failed.";
		goto error;
	}

	mVideoBackend->PublishStatus(
		mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
		mVideoBackend->OutputModelName().empty()
			? "DeckLink output device selected."
			: ("Selected output device: " + mVideoBackend->OutputModelName()));

	// Resize window to match output video frame, but scale large formats down by half for viewing.
	if (mVideoBackend->OutputFrameWidth() < 1920)
		resizeWindow(mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight());
	else
		resizeWindow(mVideoBackend->OutputFrameWidth() / 2, mVideoBackend->OutputFrameHeight() / 2);

	if (!mVideoBackend->ConfigureInput(videoModes.input, initFailureReason))
	{
		goto error;
	}
	if (!mVideoBackend->HasInputDevice())
		mVideoBackend->ReportNoInputDeviceSignalStatus();

	if (!mVideoBackend->ConfigureOutput(videoModes.output, mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(), initFailureReason))
	{
		goto error;
	}

	mVideoBackend->PublishStatus(
		mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
		mVideoBackend->StatusMessage());

	return true;

error:
	if (!initFailureReason.empty())
		MessageBoxA(NULL, initFailureReason.c_str(), "DeckLink initialization failed", MB_OK | MB_ICONERROR);
	mVideoBackend->ReleaseResources();
	return false;
}

void OpenGLComposite::paintGL(bool force)
{
	if (mRuntimeUpdateController)
		mRuntimeUpdateController->ProcessRuntimeWork();

	if (!force)
	{
		if (IsIconic(hGLWnd))
			return;
	}

	const unsigned previewFps = mRuntimeStore ? mRuntimeStore->GetConfiguredPreviewFps() : 30u;
	if (!force && mVideoBackend && mVideoBackend->ShouldPrioritizeOutputOverPreview())
	{
		ValidateRect(hGLWnd, NULL);
		return;
	}

	if (!mRenderEngine->TryPresentPreview(force, previewFps, mVideoBackend->OutputFrameWidth(), mVideoBackend->OutputFrameHeight()))
	{
		ValidateRect(hGLWnd, NULL);
		return;
	}

	ValidateRect(hGLWnd, NULL);
}

void OpenGLComposite::resizeGL(WORD width, WORD height)
{
	// We don't set the project or model matrices here since the window data is copied directly from
	// an off-screen FBO in paintGL().  Just save the width and height for use in paintGL().
	mRenderEngine->ResizeView(width, height);
}

void OpenGLComposite::resizeWindow(int width, int height)
{
	RECT r;
	if (GetWindowRect(hGLWnd, &r))
	{
		SetWindowPos(hGLWnd, HWND_TOP, r.left, r.top, r.left + width, r.top + height, 0);
	}
}

bool OpenGLComposite::InitOpenGLState()
{
	if (! ResolveGLExtensions())
		return false;

	std::string runtimeError;
	if (mRuntimeStore->GetRuntimeRepositoryRoot().empty() && !mRuntimeStore->InitializeStore(runtimeError))
	{
		MessageBoxA(NULL, runtimeError.c_str(), "Runtime host failed to initialize", MB_OK);
		return false;
	}

	if (!mRuntimeServices->Start(*this, *mRuntimeStore, runtimeError))
	{
		MessageBoxA(NULL, runtimeError.c_str(), "Runtime control services failed to start", MB_OK);
		return false;
	}

	// Prepare the runtime shader program generated from the active shader package.
	char compilerErrorMessage[1024];
	if (!mRenderEngine->CompileDecodeShader(sizeof(compilerErrorMessage), compilerErrorMessage))
	{
		MessageBoxA(NULL, compilerErrorMessage, "OpenGL decode shader failed to load or compile", MB_OK);
		return false;
	}
	if (!mRenderEngine->CompileOutputPackShader(sizeof(compilerErrorMessage), compilerErrorMessage))
	{
		MessageBoxA(NULL, compilerErrorMessage, "OpenGL output pack shader failed to load or compile", MB_OK);
		return false;
	}

	std::string rendererError;
	if (!mRenderEngine->InitializeResources(
		mVideoBackend->InputFrameWidth(),
		mVideoBackend->InputFrameHeight(),
		mVideoBackend->CaptureTextureWidth(),
		mVideoBackend->OutputFrameWidth(),
		mVideoBackend->OutputFrameHeight(),
		mVideoBackend->OutputPackTextureWidth(),
		rendererError))
	{
		MessageBoxA(NULL, rendererError.c_str(), "OpenGL initialization error.", MB_OK);
		return false;
	}

	if (!mRenderEngine->CompileLayerPrograms(mVideoBackend->InputFrameWidth(), mVideoBackend->InputFrameHeight(), sizeof(compilerErrorMessage), compilerErrorMessage))
	{
		MessageBoxA(NULL, compilerErrorMessage, "OpenGL shader failed to load or compile", MB_OK);
		return false;
	}
	mRuntimeStore->SetCompileStatus(true, "Shader layers compiled successfully.");

	mRenderEngine->ResetTemporalHistoryState();
	mRenderEngine->ResetShaderFeedbackState();

	mRuntimeUpdateController->BroadcastRuntimeState();
	mRuntimeServices->BeginPolling(*mRuntimeCoordinator);
	return true;
}

bool OpenGLComposite::Start()
{
	if (!mRenderEngine->StartRenderThread())
		return false;

	if (mRuntimeUpdateController)
		mRuntimeUpdateController->ProcessRuntimeWork();

	if (mVideoBackend->Start())
		return true;

	mRenderEngine->StopRenderThread();
	return false;
}

bool OpenGLComposite::Stop()
{
	if (mRuntimeServices)
		mRuntimeServices->Stop();

	const bool wasExternalKeyingActive = mVideoBackend->ExternalKeyingActive();
	mVideoBackend->Stop();
	if (wasExternalKeyingActive)
		mVideoBackend->PublishStatus(
			mRuntimeStore && mRuntimeStore->IsExternalKeyingConfigured(),
			"External keying has been disabled.");

	if (mRenderEngine)
		mRenderEngine->StopRenderThread();

	if (mRuntimeStore)
	{
		std::string persistenceError;
		if (!mRuntimeStore->FlushPersistenceForShutdown(std::chrono::seconds(2), persistenceError))
			OutputDebugStringA((std::string("Persistence shutdown flush failed: ") + persistenceError + "\n").c_str());
	}

	return true;
}

bool OpenGLComposite::ReloadShader(bool preserveFeedbackState)
{
	return mRuntimeCoordinator &&
		mRuntimeUpdateController &&
		mRuntimeUpdateController->ApplyRuntimeCoordinatorResult(mRuntimeCoordinator->RequestShaderReload(preserveFeedbackState));
}

bool OpenGLComposite::RequestScreenshot(std::string& error)
{
	if (!mRenderEngine || !mVideoBackend)
	{
		error = "The render engine is not ready.";
		return false;
	}

	const unsigned width = mVideoBackend->OutputFrameWidth();
	const unsigned height = mVideoBackend->OutputFrameHeight();
	if (width == 0 || height == 0)
	{
		error = "The output frame size is not available.";
		return false;
	}

	std::filesystem::path outputPath;
	try
	{
		outputPath = BuildScreenshotPath();
		std::filesystem::create_directories(outputPath.parent_path());
	}
	catch (const std::exception& exception)
	{
		error = exception.what();
		return false;
	}

	if (!mRenderEngine->RequestScreenshotCapture(
		width,
		height,
		[outputPath](unsigned captureWidth, unsigned captureHeight, std::vector<unsigned char> topDownPixels) {
			try
			{
				WritePngFileAsync(outputPath, captureWidth, captureHeight, std::move(topDownPixels));
			}
			catch (const std::exception& exception)
			{
				OutputDebugStringA((std::string("Screenshot request failed: ") + exception.what() + "\n").c_str());
			}
		}))
	{
		error = "Screenshot capture request failed.";
		return false;
	}

	return true;
}

void OpenGLComposite::renderEffect()
{
	const RenderFrameInput frameInput = BuildRenderFrameInput();
	RenderFrame(frameInput);
}

RenderFrameInput OpenGLComposite::BuildRenderFrameInput() const
{
	RenderFrameInput frameInput;
	frameInput.useCommittedLayerStates = mRuntimeCoordinator && mRuntimeCoordinator->UseCommittedLayerStates();
	frameInput.hasInputSource = mVideoBackend->HasInputSource();
	frameInput.renderWidth = mVideoBackend->InputFrameWidth();
	frameInput.renderHeight = mVideoBackend->InputFrameHeight();
	frameInput.inputFrameWidth = mVideoBackend->InputFrameWidth();
	frameInput.inputFrameHeight = mVideoBackend->InputFrameHeight();
	frameInput.captureTextureWidth = mVideoBackend->CaptureTextureWidth();
	frameInput.inputPixelFormat = mVideoBackend->InputPixelFormat();
	frameInput.historyCap = mRuntimeStore ? mRuntimeStore->GetConfiguredMaxTemporalHistoryFrames() : 0;
	frameInput.oscSmoothing = mRuntimeStore ? mRuntimeStore->GetConfiguredOscSmoothing() : 0.0;
	return frameInput;
}

void OpenGLComposite::RenderFrame(const RenderFrameInput& frameInput)
{
	RenderFrameState frameState;
	if (mRuntimeServices)
	{
		RuntimeServiceLiveBridge::PrepareLiveRenderFrameState(
			*mRuntimeServices,
			*mRenderEngine,
			frameInput,
			frameState);
	}
	else
	{
		mRenderEngine->ResolveRenderFrameState(frameInput, nullptr, frameState);
	}
	mRenderEngine->RenderPreparedFrame(frameState);
}

std::filesystem::path OpenGLComposite::BuildScreenshotPath() const
{
	const std::filesystem::path root = mRuntimeStore && !mRuntimeStore->GetRuntimeDataRoot().empty()
		? mRuntimeStore->GetRuntimeDataRoot()
		: std::filesystem::current_path();

	const auto now = std::chrono::system_clock::now();
	const auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
	const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
	std::tm localTime = {};
	localtime_s(&localTime, &nowTime);

	std::ostringstream filename;
	filename << "video-shader-toys-"
		<< std::put_time(&localTime, "%Y%m%d-%H%M%S")
		<< "-" << std::setw(3) << std::setfill('0') << milliseconds.count()
		<< ".png";

	return root / "screenshots" / filename.str();
}

bool OpenGLComposite::CheckOpenGLExtensions()
{
	return true;
}
