#include "HyperTwistDiagnostics/HyperTwistRuntimeDiagnostics.h"

#include "Containers/Ticker.h"
#include "Engine/Engine.h"
#include "Engine/GameViewportClient.h"
#include "HAL/CriticalSection.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformProcess.h"
#include "HAL/PlatformTime.h"
#include "Misc/CommandLine.h"
#include "Misc/DateTime.h"
#include "Misc/FileHelper.h"
#include "Misc/Parse.h"
#include "Misc/Paths.h"
#include "Misc/ScopeLock.h"
#include "UnrealClient.h"

DEFINE_LOG_CATEGORY_STATIC(LogHyperTwistRuntimeDiagnostics, Log, All);

namespace
{
	constexpr int32 ReadyFrameWarmupPassCount = 3;
	constexpr double ReadyFrameInterCaptureDelaySeconds = 0.75;
	constexpr double ReadyFrameReceiptTimeoutSeconds = 10.0;
	const TCHAR* DefaultDiagnosticsLogFileName = TEXT("HyperTwistRuntime-latest.log");

	FCriticalSection DiagnosticsLock;
	FString DiagnosticsLogPath;
	bool bDiagnosticsInitialized = false;

	FString MakeSingleLine(FString Value)
	{
		Value.ReplaceInline(TEXT("\r"), TEXT(" "));
		Value.ReplaceInline(TEXT("\n"), TEXT(" "));
		return Value.TrimStartAndEnd();
	}

	bool InitializeSessionLocked()
	{
		if (bDiagnosticsInitialized)
		{
			return !DiagnosticsLogPath.IsEmpty();
		}

		bDiagnosticsInitialized = true;
		FString RequestedPath;
		FParse::Value(
			FCommandLine::Get(),
			TEXT("HyperTwistDiagnosticsLog="),
			RequestedPath);
		RequestedPath.TrimStartAndEndInline();
		if (RequestedPath.Len() >= 2
			&& RequestedPath.StartsWith(TEXT("\""))
			&& RequestedPath.EndsWith(TEXT("\"")))
		{
			RequestedPath = RequestedPath.Mid(1, RequestedPath.Len() - 2);
		}

		DiagnosticsLogPath = RequestedPath.IsEmpty()
			? FPaths::Combine(FPaths::ProjectLogDir(), DefaultDiagnosticsLogFileName)
			: FPaths::ConvertRelativePathToFull(RequestedPath);
		FPaths::NormalizeFilename(DiagnosticsLogPath);

		const FString DiagnosticsDirectory = FPaths::GetPath(DiagnosticsLogPath);
		if (DiagnosticsDirectory.IsEmpty()
			|| !IFileManager::Get().MakeDirectory(*DiagnosticsDirectory, true))
		{
			DiagnosticsLogPath.Reset();
			return false;
		}

		TArray<FString> HeaderLines;
		HeaderLines.Reserve(5);
		HeaderLines.Add(TEXT("format=hypertwist-runtime-diagnostics/v1"));
		HeaderLines.Add(FString::Printf(
			TEXT("session=%s"),
			*FDateTime::UtcNow().ToString(TEXT("%Y%m%dT%H%M%SZ"))));
		HeaderLines.Add(FString::Printf(
			TEXT("process_id=%u"),
			FPlatformProcess::GetCurrentProcessId()));
		HeaderLines.Add(FString::Printf(
			TEXT("command_line=%s"),
			*MakeSingleLine(FCommandLine::Get())));
		HeaderLines.Add(FString::Printf(
			TEXT("diagnostics_path=%s"),
			*DiagnosticsLogPath));

		if (!FFileHelper::SaveStringArrayToFile(
				HeaderLines,
				*DiagnosticsLogPath,
				FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
		{
			DiagnosticsLogPath.Reset();
			return false;
		}

		return true;
	}
}

namespace HyperTwistRuntimeDiagnostics
{
	bool InitializeSession()
	{
		const FScopeLock Lock(&DiagnosticsLock);
		return InitializeSessionLocked();
	}

	FString GetDiagnosticsLogPath()
	{
		const FScopeLock Lock(&DiagnosticsLock);
		InitializeSessionLocked();
		return DiagnosticsLogPath;
	}

	bool AppendEvent(const FString& Stage, const FString& Message)
	{
		const FScopeLock Lock(&DiagnosticsLock);
		if (!InitializeSessionLocked())
		{
			return false;
		}

		const FString Line = FString::Printf(
			TEXT("[%s] [%s] %s%s"),
			*FDateTime::UtcNow().ToString(TEXT("%Y-%m-%dT%H:%M:%SZ")),
			*MakeSingleLine(Stage.IsEmpty() ? TEXT("Runtime") : Stage),
			*MakeSingleLine(Message),
			LINE_TERMINATOR);
		return FFileHelper::SaveStringToFile(
			Line,
			*DiagnosticsLogPath,
			FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM,
			&IFileManager::Get(),
			FILEWRITE_Append);
	}

	bool IsReadyFrameCaptureRequested()
	{
		return FParse::Param(FCommandLine::Get(), TEXT("HyperTwistCaptureWhenReady"));
	}

	bool RequestReadyFrameCapture(const FString& SurfaceId)
	{
		if (!IsReadyFrameCaptureRequested())
		{
			return false;
		}

		FString SafeSurfaceId = SurfaceId.IsEmpty() ? TEXT("runtime-ready") : SurfaceId;
		for (TCHAR& Character : SafeSurfaceId)
		{
			if (!FChar::IsAlnum(Character) && Character != TEXT('-') && Character != TEXT('_'))
			{
				Character = TEXT('-');
			}
		}

		const FString CaptureDirectory = FPaths::Combine(
			FPaths::ProjectSavedDir(),
			TEXT("Screenshots"),
			TEXT("HyperTwistDiagnostics"));
		if (!IFileManager::Get().MakeDirectory(*CaptureDirectory, true))
		{
			UE_LOG(
				LogHyperTwistRuntimeDiagnostics,
				Error,
				TEXT("Could not create semantic-ready capture directory '%s'."),
				*CaptureDirectory);
			return false;
		}

		const FString CapturePath = FPaths::Combine(
			CaptureDirectory,
			FString::Printf(TEXT("%s.png"), *SafeSurfaceId));
		const FString WarmupCapturePath = FPaths::Combine(
			CaptureDirectory,
			FString::Printf(TEXT("%s-warmup.png"), *SafeSurfaceId));
		if (IFileManager::Get().FileExists(*CapturePath)
			&& !IFileManager::Get().Delete(*CapturePath, false, true))
		{
			UE_LOG(
				LogHyperTwistRuntimeDiagnostics,
				Error,
				TEXT("Could not replace stale semantic-ready frame '%s'."),
				*CapturePath);
			return false;
		}
		if (IFileManager::Get().FileExists(*WarmupCapturePath)
			&& !IFileManager::Get().Delete(*WarmupCapturePath, false, true))
		{
			UE_LOG(
				LogHyperTwistRuntimeDiagnostics,
				Error,
				TEXT("Could not replace stale semantic-ready warm-up frame '%s'."),
				*WarmupCapturePath);
			return false;
		}

		// Repeated UI-inclusive captures prime and settle Slate's font atlas in
		// packaged off-screen rendering. A single warm-up can still leave random
		// glyphs absent, so the authoritative frame follows three acknowledged
		// passes separated by real presentation time. File polling remains
		// necessary because this viewport does not reliably deliver the
		// screenshot-processed delegate.
		UE_LOG(
			LogHyperTwistRuntimeDiagnostics,
			Display,
			TEXT("Queued UI-inclusive semantic-ready frame for %s after presentation stabilization."),
			*SurfaceId);
		FTSTicker::GetCoreTicker().AddTicker(
			FTickerDelegate::CreateLambda(
				[
					CapturePath,
					WarmupCapturePath,
					SurfaceId,
					WarmupPassesCompleted = 0,
					bWarmupRequestInFlight = false,
					bFinalRequested = false,
					NextCaptureRequestAt = 0.0,
					ReceiptDeadline = 0.0
				](const float DeltaTime) mutable
		{
			static_cast<void>(DeltaTime);
			const double Now = FPlatformTime::Seconds();
			if (WarmupPassesCompleted < ReadyFrameWarmupPassCount)
			{
				if (!bWarmupRequestInFlight)
				{
					if (Now < NextCaptureRequestAt)
					{
						return true;
					}
					if (IFileManager::Get().FileExists(*WarmupCapturePath)
						&& !IFileManager::Get().Delete(*WarmupCapturePath, false, true))
					{
						UE_LOG(
							LogHyperTwistRuntimeDiagnostics,
							Error,
							TEXT("Could not reset semantic-ready warm-up frame '%s'."),
							*WarmupCapturePath);
						return false;
					}

					FScreenshotRequest::RequestScreenshot(
						WarmupCapturePath,
						true,
						false,
						false);
					bWarmupRequestInFlight = true;
					ReceiptDeadline = Now + ReadyFrameReceiptTimeoutSeconds;
					UE_LOG(
						LogHyperTwistRuntimeDiagnostics,
						Display,
						TEXT("Requested UI-inclusive semantic-ready warm-up frame %d/%d for %s at '%s'."),
						WarmupPassesCompleted + 1,
						ReadyFrameWarmupPassCount,
						*SurfaceId,
						*WarmupCapturePath);
					return true;
				}

				if (IFileManager::Get().FileSize(*WarmupCapturePath) > 0)
				{
					if (!IFileManager::Get().Delete(*WarmupCapturePath, false, true))
					{
						UE_LOG(
							LogHyperTwistRuntimeDiagnostics,
							Error,
							TEXT("Could not retire semantic-ready warm-up frame '%s'."),
							*WarmupCapturePath);
						return false;
					}

					++WarmupPassesCompleted;
					bWarmupRequestInFlight = false;
					NextCaptureRequestAt = Now + ReadyFrameInterCaptureDelaySeconds;
					UE_LOG(
						LogHyperTwistRuntimeDiagnostics,
						Display,
						TEXT("Completed UI-inclusive semantic-ready warm-up frame %d/%d for %s."),
						WarmupPassesCompleted,
						ReadyFrameWarmupPassCount,
						*SurfaceId);
					return true;
				}

				if (Now >= ReceiptDeadline)
				{
					UE_LOG(
						LogHyperTwistRuntimeDiagnostics,
						Error,
						TEXT("Could not save UI-inclusive semantic-ready warm-up frame %d/%d for %s at '%s'."),
						WarmupPassesCompleted + 1,
						ReadyFrameWarmupPassCount,
						*SurfaceId,
						*WarmupCapturePath);
					return false;
				}
				return true;
			}

			if (!bFinalRequested)
			{
				if (Now < NextCaptureRequestAt)
				{
					return true;
				}

				FScreenshotRequest::RequestScreenshot(
					CapturePath,
					true,
					false,
					false);
				bFinalRequested = true;
				ReceiptDeadline = Now + ReadyFrameReceiptTimeoutSeconds;
				UE_LOG(
					LogHyperTwistRuntimeDiagnostics,
					Display,
					TEXT("Requested UI-inclusive semantic-ready frame for %s at '%s' after font-atlas warm-up."),
					*SurfaceId,
					*CapturePath);
				return true;
			}

			if (IFileManager::Get().FileSize(*CapturePath) > 0)
			{
				UE_LOG(
					LogHyperTwistRuntimeDiagnostics,
					Display,
					TEXT("Saved UI-inclusive semantic-ready frame for %s at '%s'."),
					*SurfaceId,
					*CapturePath);
				return false;
			}
			if (Now >= ReceiptDeadline)
			{
				UE_LOG(
					LogHyperTwistRuntimeDiagnostics,
					Error,
					TEXT("Could not save UI-inclusive semantic-ready frame for %s at '%s'."),
					*SurfaceId,
					*CapturePath);
				return false;
			}
			return true;
			}),
			0.5f);
		return true;
	}
}
