#include "PreviewWindowThread.h"

#include "../frames/SystemFrameExchange.h"
#include "../logging/Logger.h"

#include <algorithm>
#include <cstring>

namespace RenderCadenceCompositor
{
namespace
{
const char* kPreviewWindowClassName = "RenderCadencePreviewWindow";
constexpr UINT_PTR kPreviewTimerId = 1;
constexpr UINT kPreviewStopMessage = WM_APP + 1;

void RegisterPreviewWindowClass()
{
	static bool registered = false;
	if (registered)
		return;

	WNDCLASSA windowClass = {};
	windowClass.lpfnWndProc = PreviewWindowThread::WindowProc;
	windowClass.hInstance = GetModuleHandleA(nullptr);
	windowClass.lpszClassName = kPreviewWindowClassName;
	windowClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
	windowClass.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
	RegisterClassA(&windowClass);
	registered = true;
}
}

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

bool PreviewWindowThread::Start(SystemFrameExchange& exchange, const PreviewWindowConfig& config, std::string& error)
{
	if (!config.enabled)
		return true;
	if (mThread.joinable())
	{
		error = "Preview window thread is already running.";
		return false;
	}

	mExchange = &exchange;
	mConfig = config;
	mStopRequested.store(false, std::memory_order_release);
	mThread = std::thread(&PreviewWindowThread::ThreadMain, this);
	return true;
}

void PreviewWindowThread::Stop()
{
	mStopRequested.store(true, std::memory_order_release);
	const DWORD threadId = mThreadId.load(std::memory_order_acquire);
	if (threadId != 0)
		PostThreadMessageA(threadId, kPreviewStopMessage, 0, 0);
	if (mThread.joinable())
		mThread.join();
	mExchange = nullptr;
	mRunning.store(false, std::memory_order_release);
}

LRESULT CALLBACK PreviewWindowThread::WindowProc(HWND window, UINT message, WPARAM wParam, LPARAM lParam)
{
	PreviewWindowThread* owner = reinterpret_cast<PreviewWindowThread*>(GetWindowLongPtrA(window, GWLP_USERDATA));
	if (message == WM_NCCREATE)
	{
		const CREATESTRUCTA* create = reinterpret_cast<const CREATESTRUCTA*>(lParam);
		owner = reinterpret_cast<PreviewWindowThread*>(create->lpCreateParams);
		SetWindowLongPtrA(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(owner));
	}

	switch (message)
	{
	case WM_PAINT:
		if (owner != nullptr)
		{
			owner->Paint(window);
			return 0;
		}
		break;
	case WM_TIMER:
		if (owner != nullptr && wParam == kPreviewTimerId)
		{
			InvalidateRect(window, nullptr, FALSE);
			return 0;
		}
		break;
	case WM_CLOSE:
		if (owner != nullptr)
		{
			if (!owner->mStopRequested.load(std::memory_order_acquire))
				TryLog(LogLevel::Log, "preview", "Preview window closed by user.");
			owner->mStopRequested.store(true, std::memory_order_release);
		}
		DestroyWindow(window);
		return 0;
	case WM_DESTROY:
		if (owner != nullptr)
			owner->mWindow = nullptr;
		PostQuitMessage(0);
		return 0;
	default:
		break;
	}

	return DefWindowProcA(window, message, wParam, lParam);
}

void PreviewWindowThread::ThreadMain()
{
	MSG message = {};
	PeekMessageA(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
	mThreadId.store(GetCurrentThreadId(), std::memory_order_release);
	mRunning.store(true, std::memory_order_release);

	std::string error;
	if (!CreatePreviewWindow(error))
	{
		TryLog(LogLevel::Error, "preview", error.empty() ? "Preview window creation failed." : error);
		mThreadId.store(0, std::memory_order_release);
		mRunning.store(false, std::memory_order_release);
		return;
	}

	if (!mStopRequested.load(std::memory_order_acquire))
	{
		if (SetTimer(mWindow, kPreviewTimerId, PreviewTimerIntervalMilliseconds(mConfig.fps), nullptr) == 0)
			TryLog(LogLevel::Error, "preview", "Preview window timer could not be started.");
		else
			TryLog(LogLevel::Log, "preview", "Preview window started.");
	}

	while (!mStopRequested.load(std::memory_order_acquire))
	{
		const BOOL result = GetMessageA(&message, nullptr, 0, 0);
		if (result <= 0)
			break;
		if (message.message == kPreviewStopMessage)
		{
			mStopRequested.store(true, std::memory_order_release);
			break;
		}
		TranslateMessage(&message);
		DispatchMessageA(&message);
	}

	if (mWindow != nullptr)
	{
		KillTimer(mWindow, kPreviewTimerId);
		DestroyWindow(mWindow);
		mWindow = nullptr;
	}

	mThreadId.store(0, std::memory_order_release);
	mRunning.store(false, std::memory_order_release);
	TryLog(LogLevel::Log, "preview", "Preview window thread stopped.");
}

bool PreviewWindowThread::CreatePreviewWindow(std::string& error)
{
	RegisterPreviewWindowClass();

	mWindow = CreateWindowExA(
		0,
		kPreviewWindowClassName,
		mConfig.title.c_str(),
		WS_OVERLAPPEDWINDOW,
		CW_USEDEFAULT,
		CW_USEDEFAULT,
		960,
		540,
		nullptr,
		nullptr,
		GetModuleHandleA(nullptr),
		this);

	if (mWindow == nullptr)
	{
		error = "CreateWindowEx failed for preview window.";
		return false;
	}

	ShowWindow(mWindow, SW_SHOW);
	UpdateWindow(mWindow);
	return true;
}

void PreviewWindowThread::Paint(HWND window)
{
	PAINTSTRUCT paint = {};
	HDC dc = BeginPaint(window, &paint);

	RECT client = {};
	GetClientRect(window, &client);
	const int clientWidth = std::max(1L, client.right - client.left);
	const int clientHeight = std::max(1L, client.bottom - client.top);

	SystemFrame frame;
	const bool frameAcquired = mExchange != nullptr && mExchange->TryAcquireLatestForPreview(frame);
	const bool canPaintFrame =
		frameAcquired &&
		frame.bytes != nullptr &&
		frame.pixelFormat == VideoIOPixelFormat::Bgra8 &&
		frame.width > 0 &&
		frame.height > 0;
	if (canPaintFrame)
	{
		if (CopyPreviewOrientedBgra8Frame(frame))
		{
			const int previousStretchMode = SetStretchBltMode(dc, HALFTONE);
			POINT previousBrushOrigin = {};
			SetBrushOrgEx(dc, 0, 0, &previousBrushOrigin);

			BITMAPINFO bitmapInfo = {};
			bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
			bitmapInfo.bmiHeader.biWidth = static_cast<LONG>(frame.width);
			bitmapInfo.bmiHeader.biHeight = -static_cast<LONG>(frame.height);
			bitmapInfo.bmiHeader.biPlanes = 1;
			bitmapInfo.bmiHeader.biBitCount = 32;
			bitmapInfo.bmiHeader.biCompression = BI_RGB;

			StretchDIBits(
				dc,
				0,
				0,
				clientWidth,
				clientHeight,
				0,
				0,
				static_cast<int>(frame.width),
				static_cast<int>(frame.height),
				mPaintPixels.data(),
				&bitmapInfo,
				DIB_RGB_COLORS,
				SRCCOPY);

			if (previousStretchMode != 0)
				SetStretchBltMode(dc, previousStretchMode);
			SetBrushOrgEx(dc, previousBrushOrigin.x, previousBrushOrigin.y, nullptr);
		}
		else
		{
			if (!mLoggedPaintCopyFailure)
			{
				TryLog(LogLevel::Warning, "preview", "Preview frame could not be copied for mirrored presentation.");
				mLoggedPaintCopyFailure = true;
			}
			FillRect(dc, &client, reinterpret_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
		}
	}
	else
	{
		FillRect(dc, &client, reinterpret_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
	}

	if (frameAcquired)
		mExchange->ReleasePreviewFrame(frame);

	EndPaint(window, &paint);
}

bool PreviewWindowThread::CopyPreviewOrientedBgra8Frame(const SystemFrame& frame)
{
	constexpr std::size_t kBgraBytesPerPixel = 4;
	if (frame.bytes == nullptr || frame.width == 0 || frame.height == 0 || frame.rowBytes <= 0)
		return false;

	const std::size_t width = static_cast<std::size_t>(frame.width);
	const std::size_t height = static_cast<std::size_t>(frame.height);
	const std::size_t sourceRowBytes = static_cast<std::size_t>(frame.rowBytes);
	const std::size_t destinationRowBytes = width * kBgraBytesPerPixel;
	if (sourceRowBytes < destinationRowBytes)
		return false;

	mPaintPixels.resize(destinationRowBytes * height);
	const unsigned char* sourceBytes = static_cast<const unsigned char*>(frame.bytes);
	for (std::size_t y = 0; y < height; ++y)
	{
		const unsigned char* sourceRow = sourceBytes + (height - 1u - y) * sourceRowBytes;
		unsigned char* destinationRow = mPaintPixels.data() + y * destinationRowBytes;
		for (std::size_t x = 0; x < width; ++x)
		{
			const unsigned char* sourcePixel = sourceRow + x * kBgraBytesPerPixel;
			unsigned char* destinationPixel = destinationRow + x * kBgraBytesPerPixel;
			std::memcpy(destinationPixel, sourcePixel, kBgraBytesPerPixel);
		}
	}

	return true;
}
}
