/**
 * Copyright 2022-2025 Theai, Inc. dba Inworld AI
 */

#include "Debug/GameplayDebuggerCategory_Inworld.h"

#if WITH_GAMEPLAY_DEBUGGER

#include "CanvasItem.h"
#include "Dom/JsonObject.h"
#include "Engine/Canvas.h"
#include "Engine/Texture2D.h"
#include "GameFramework/PlayerController.h"
#include "GameplayDebuggerCategoryReplicator.h"
#include "GlobalRenderResources.h"
#include "InputCoreTypes.h"
#include "InworldRuntimeSubsystem.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"

FGameplayDebuggerCategory_Inworld::FGameplayDebuggerCategory_Inworld()
{
	BindKeyPress(EKeys::BackSpace.GetFName(), this, &FGameplayDebuggerCategory_Inworld::OnClearHistory);
	BindKeyPress(EKeys::PageDown.GetFName(), this, &FGameplayDebuggerCategory_Inworld::OnHistoryIndexIncrement);
	BindKeyPress(EKeys::PageUp.GetFName(), this, &FGameplayDebuggerCategory_Inworld::OnHistoryIndexDecrement);

	BindKeyPress(EKeys::PageUp.GetFName(), FGameplayDebuggerInputModifier::Shift, this,
		&FGameplayDebuggerCategory_Inworld::OnOwnerIndexIncrement);
	BindKeyPress(EKeys::PageDown.GetFName(), FGameplayDebuggerInputModifier::Shift, this,
		&FGameplayDebuggerCategory_Inworld::OnOwnerIndexDecrement);

	SetDataPackReplication<FRepData>(&DataPack);
}

TSharedRef<FGameplayDebuggerCategory> FGameplayDebuggerCategory_Inworld::MakeInstance()
{
	return MakeShareable(new FGameplayDebuggerCategory_Inworld());
}

void FGameplayDebuggerCategory_Inworld::FRepData::Serialize(FArchive& Ar)
{
	Ar << MaxOwnerNum;

	int32 NumOld = OldHistoryList.Num();
	Ar << NumOld;
	if (Ar.IsLoading())
	{
		OldHistoryList.SetNum(NumOld);
	}
	for (int32 Idx = 0; Idx < NumOld; Idx++)
	{
		Ar << OldHistoryList[Idx].GraphName;
		Ar << OldHistoryList[Idx].ExecutionId;
		Ar << OldHistoryList[Idx].ErrorMessage;
	}

	Ar << GraphName;
	Ar << ExecutionId;
	Ar << ErrorMessage;

	int32 NumNodes = NodeDebugList.Num();
	Ar << NumNodes;
	if (Ar.IsLoading())
	{
		NodeDebugList.SetNum(NumNodes);
	}

	for (int32 Idx = 0; Idx < NumNodes; Idx++)
	{
		Ar << NodeDebugList[Idx].NodeName;
		Ar << NodeDebugList[Idx].NodeTime;
		Ar << NodeDebugList[Idx].NodeDetail;
	}
}

void FGameplayDebuggerCategory_Inworld::OnClearHistory()
{
	if (UInworldRuntimeSubsystem* RuntimeSubsystem = UInworldRuntimeSubsystem::Get())
	{
		RuntimeSubsystem->ClearGraphExecutionHistory();
	}
}

void FGameplayDebuggerCategory_Inworld::OnHistoryIndexIncrement()
{
	ChangeHistoryIndex(1);
}

void FGameplayDebuggerCategory_Inworld::OnHistoryIndexDecrement()
{
	ChangeHistoryIndex(-1);
}

void FGameplayDebuggerCategory_Inworld::ChangeHistoryIndex(const int32 Step)
{
	if (UInworldRuntimeSubsystem* RuntimeSubsystem = UInworldRuntimeSubsystem::Get())
	{
		UObject* HistoryOwner = GetHistoryOwner();

		if (HistoryOwner)
		{
			TArray<const FInworldGraphExecutionHistory*> GraphHistoryList;
			RuntimeSubsystem->GetAllExecutionHistory(HistoryOwner, GraphHistoryList);	 // So we have our local subset of data.

			const int32 TotalCount = GraphHistoryList.Num();
			if (GraphHistoryList.Num())
			{
				int32 NextIndex = HistoryIndex + Step;

				if ((NextIndex == INDEX_NONE) || (NextIndex == TotalCount))
				{
					HistoryIndex = INDEX_NONE;
				}
				else if (HistoryIndex == INDEX_NONE)
				{
					HistoryIndex = (Step < 0) ? TotalCount - 1 : 0;
				}
				else
				{
					HistoryIndex = (HistoryIndex + Step + TotalCount) % TotalCount;
				}
			}
		}
		else
		{
			HistoryIndex = INDEX_NONE;
		}
	}
}

void FGameplayDebuggerCategory_Inworld::OnOwnerIndexIncrement()
{
	HistoryIndex = INDEX_NONE;
	ChangeOwnerIndex(1);
}

void FGameplayDebuggerCategory_Inworld::OnOwnerIndexDecrement()
{
	HistoryIndex = INDEX_NONE;
	ChangeOwnerIndex(-1);
}

void FGameplayDebuggerCategory_Inworld::ChangeOwnerIndex(const int32 Step)
{
	if (UInworldRuntimeSubsystem* RuntimeSubsystem = UInworldRuntimeSubsystem::Get())
	{
		TArray<UObject*> OwnerList;
		RuntimeSubsystem->GetAllUniqueOwner(OwnerList);

		const int32 TotalCount = OwnerList.Num();
		if (OwnerList.Num())
		{
			int32 NextIndex = HistoryOwnerIndex + Step;

			if ((NextIndex == INDEX_NONE) || (NextIndex == TotalCount))
			{
				HistoryOwnerIndex = INDEX_NONE;
			}
			else if (HistoryOwnerIndex == INDEX_NONE)
			{
				HistoryOwnerIndex = (Step < 0) ? TotalCount - 1 : 0;
			}
			else
			{
				HistoryOwnerIndex = (HistoryOwnerIndex + Step + TotalCount) % TotalCount;
			}
		}
	}
}

UObject* FGameplayDebuggerCategory_Inworld::GetHistoryOwner()
{
	UObject* HistoryOwner = OwnerWeakPtr.Get();

	if ((HistoryOwner == nullptr) && (HistoryOwnerIndex == INDEX_NONE))
	{
		HistoryOwnerIndex = 0;
	}
	if (HistoryOwnerIndex != INDEX_NONE)
	{
		if (UInworldRuntimeSubsystem* RuntimeSubsystem = UInworldRuntimeSubsystem::Get())
		{
			TArray<UObject*> OwnerList;
			RuntimeSubsystem->GetAllUniqueOwner(OwnerList);

			if (OwnerList.IsValidIndex(HistoryOwnerIndex))
			{
				HistoryOwner = OwnerList[HistoryOwnerIndex];
			}
		}
	}
	if (AActor* HistoryActor = Cast<AActor>(HistoryOwner))
	{
		GetReplicator()->SetDebugActor(HistoryActor);
	}

	return HistoryOwner;
}

void FGameplayDebuggerCategory_Inworld::CollectData(APlayerController* OwnerPC, AActor* DebugActor)
{
	if (UInworldRuntimeSubsystem* RuntimeSubsystem = UInworldRuntimeSubsystem::Get())
	{
		TArray<UObject*> OwnerList;
		RuntimeSubsystem->GetAllUniqueOwner(OwnerList);
		DataPack.MaxOwnerNum = OwnerList.Num();

		// Keeps track of 'selected' actor
		if (DebugActor == nullptr)
		{
			GetReplicator()->SetDebugActor(OwnerPC);
		}
		OwnerWeakPtr = MakeWeakObjectPtr(DebugActor);

		UObject* HistoryOwner = GetHistoryOwner();

		TArray<const FInworldGraphExecutionHistory*> GraphHistoryList;
		RuntimeSubsystem->GetAllExecutionHistory(HistoryOwner, GraphHistoryList);	 // So we have our local subset of data.
		if (GraphHistoryList.Num())
		{
			// FDateTime DateTime = FDateTime::FromUnixTimestamp(GraphHistory->StartTime);
			for (int32 i = 0; i < GraphHistoryList.Num(); ++i)
			{
				const FInworldGraphExecutionHistory* GraphHistory = GraphHistoryList[i];
				if (GraphHistory)
				{
					bool bIsOldHistory = (((HistoryIndex == INDEX_NONE) && (i != GraphHistoryList.Num() - 1)) ||
										  ((HistoryIndex != INDEX_NONE) && (i != HistoryIndex)));

					FRepData::FOldHistory& OldHistory = DataPack.OldHistoryList.AddDefaulted_GetRef();
					OldHistory.GraphName = GraphHistory->GraphId;
					OldHistory.ExecutionId = GraphHistory->ExecutionId;
					OldHistory.ErrorMessage = GraphHistory->GetLastError();

					if (!bIsOldHistory)
					{
						DataPack.GraphName = GraphHistory->GraphId;
						DataPack.ExecutionId = GraphHistory->ExecutionId;
						DataPack.ErrorMessage = GraphHistory->GetLastError();

						int32 totalNode = DataPack.NodeDebugList.Num();
						DataPack.NodeDebugList.Empty(totalNode);
						for (const FInworldNodeHistory& Node : GraphHistory->NodeHistory)
						{
							FRepData::FNodeDebug& NodeDebug = DataPack.NodeDebugList.AddDefaulted_GetRef();
							NodeDebug.NodeName = Node.Name;
							NodeDebug.NodeTime = Node.DurationInMilliSeconds;

							if (!Node.OutputAsJson.IsEmpty())
							{
								TSharedPtr<FJsonObject> JsonObject;
								TSharedRef<TJsonReader<>> JsonReader = TJsonReaderFactory<>::Create(Node.OutputAsJson);
								if (FJsonSerializer::Deserialize(JsonReader, JsonObject) && JsonObject.IsValid())
								{
									const TSharedPtr<FJsonObject>* DataObject;
									if (JsonObject->TryGetObjectField(TEXT("data"), DataObject))
									{
										FString ExtractedText;
										if ((*DataObject)->TryGetStringField(TEXT("text"), ExtractedText))
										{
											// Replace all newlines with spaces
											ExtractedText =
												ExtractedText.Replace(TEXT("\r"), TEXT(" ")).Replace(TEXT("\n"), TEXT(" "));

											NodeDebug.NodeDetail = ExtractedText;
										}
									}
								}
							}
						}
					}
				}
			}
		}
	}
}

void FGameplayDebuggerCategory_Inworld::DrawData(APlayerController* OwnerPC, FGameplayDebuggerCanvasContext& CanvasContext)
{
	// Before doing anything, ensure the canvas is valid.
	if (!CanvasContext.Canvas.IsValid() || !GWhiteTexture)
	{
		return;
	}

	// --- 1. Dynamically calculate the background box properties ---
	const FVector2D ViewportSize(CanvasContext.Canvas->ClipX, CanvasContext.Canvas->ClipY);
	const FColor BackgroundColor(0, 0, 0, 200);
	const FVector2D BoxPos(CanvasContext.CursorX, CanvasContext.CursorY);	 // Start with a small padding from the edge

	const float BoxWidth = ViewportSize.X - BoxPos.X - 10.f;
	const float BoxHeight = ViewportSize.Y * 0.8f;
	const FVector2D BoxSize(BoxWidth, BoxHeight);

	// --- 2. Draw the background box ---
	FCanvasTileItem TileItem(BoxPos, GWhiteTexture, BoxSize, BackgroundColor);
	TileItem.BlendMode = SE_BLEND_Translucent;
	CanvasContext.Canvas->DrawItem(TileItem);

	// --- 3. Set cursor position inside the new box ---
	CanvasContext.CursorX = BoxPos.X + 5.f;	   // Add a little internal padding
	CanvasContext.CursorY = BoxPos.Y + 5.f;

	if (DataPack.MaxOwnerNum == 0)
	{
		CanvasContext.Printf(TEXT("No Graph History to Inspect. Execute a Graph to start seeing information."));
		return;
	}
	else if (DataPack.OldHistoryList.Num() == 0)
	{
		UObject* HistoryOwner = GetHistoryOwner();
		if (HistoryOwner)
		{
			CanvasContext.Printf(TEXT("Selected Actor: %s has no Graph History to Inspect. (Select Actor or Press Key "
									  "Shift+PgUp/PgDn to cycle actors)"),
				*HistoryOwner->GetName());
		}
		else
		{
			CanvasContext.Printf(
				TEXT("Viewing {yellow}%d{white} of {yellow}%d{white} (Select Actor or Press Key Shift+PgUp/PgDn to cycle actors)"),
				HistoryOwnerIndex, DataPack.MaxOwnerNum);
		}
		return;
	}

	CanvasContext.Printf(TEXT("{white} Press key {yellow} Backspace {white}to erase ALL history"));

	// Print header information
	UObject* HistoryOwner = GetHistoryOwner();
	if (HistoryOwner)
	{
		CanvasContext.Printf(
			TEXT("Viewing %s:  {yellow}%d{white} of {yellow}%d{white} (Select Actor or Press Key Shift+PgUp/PgDn to cycle actors)"),
			*HistoryOwner->GetName(), HistoryOwnerIndex, DataPack.MaxOwnerNum);
	}
	else
	{
		CanvasContext.Printf(
			TEXT(
				"Viewing Owner {yellow}%d{white} of {yellow}%d{white} (Select Actor or Press Key Shift+PgUp/PgDn to cycle actors)"),
			HistoryOwnerIndex, DataPack.MaxOwnerNum);
	}

	if (HistoryIndex == INDEX_NONE)
	{
		CanvasContext.Printf(TEXT("{yellow}ALWAYS LAST GRAPH {white} of {yellow}%d{white} (Press Key PgUp/PgDn to cycle history)"),
			DataPack.OldHistoryList.Num());
	}
	else
	{
		CanvasContext.Printf(TEXT("History {yellow}%d{white} of {yellow}%d{white} (Press Key PgUp/PgDn to cycle history)"),
			HistoryIndex, DataPack.OldHistoryList.Num());
	}

	// --- Store positions for column layout ---
	const float InitialY = CanvasContext.CursorY;
	const float ColumnWidth = 400.f;	// Adjust this value to change the spacing between columns

	// --- Determine the actually selected index (handles INDEX_NONE case) ---
	const int32 SelectedHistoryIndex = (HistoryIndex == INDEX_NONE) ? (DataPack.OldHistoryList.Num() - 1) : HistoryIndex;

	// ##
	// ## Draw Left Column: The list of all graph execution histories
	// ##
	CanvasContext.Printf(TEXT("{white}Execution History:"));
	for (int32 i = 0; i < DataPack.OldHistoryList.Num(); ++i)
	{
		const bool bIsSelected = (i == SelectedHistoryIndex);
		const FColor& LineColor = bIsSelected										  ? FColorList::Yellow
								  : DataPack.OldHistoryList[i].ErrorMessage.IsEmpty() ? FColorList::Goldenrod
																					  : FColor::Red;

		CanvasContext.Printf(
			LineColor, TEXT("[%d] %s - %s"), i, *DataPack.OldHistoryList[i].GraphName, *DataPack.OldHistoryList[i].ExecutionId);
	}

	// ##
	// ## Draw Right Column: Details for the currently selected history
	// ##
	CanvasContext.CursorX = ColumnWidth;
	CanvasContext.CursorY = InitialY;

	// Check if the selected data is valid before trying to draw it
	if (DataPack.ExecutionId.IsEmpty() == false)
	{
		CanvasContext.Printf(TEXT("{yellow}Selected Details: %s - %s"), *DataPack.GraphName, *DataPack.ExecutionId);

		if (!DataPack.ErrorMessage.IsEmpty())
		{
			CanvasContext.Printf(TEXT("{red}Last Error: %s"), *DataPack.ErrorMessage);
		}

		for (int32 j = 0; j < DataPack.NodeDebugList.Num(); ++j)
		{
			// We must reset CursorX for every line printed in this column.
			CanvasContext.CursorX = ColumnWidth;

			const FRepData::FNodeDebug& NodeData = DataPack.NodeDebugList[j];
			if (NodeData.NodeDetail.IsEmpty())
			{
				CanvasContext.Printf(TEXT("{white}%s: %.2f ms"), *NodeData.NodeName, NodeData.NodeTime);
			}
			else
			{
				CanvasContext.Printf(TEXT("{white}%s: %.2f ms - %s"), *NodeData.NodeName, NodeData.NodeTime, *NodeData.NodeDetail);
			}
		}
	}
}

#endif	  // WITH_GAMEPLAY_DEBUGGER_MENU
