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

#include "InworldCharacterSubsystem.h"

#include "AssetRegistry/AssetRegistryModule.h"
#include "Components/InworldConversationGroupComponent.h"
#include "Components/InworldConversationTargetComponent.h"
#include "GameFramework/PlayerController.h"
#include "InworldCharacter.h"

void UInworldCharacterSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
	Super::Initialize(Collection);

	ConversationTargetRegisterDelegateHandle =
		UInworldConversationTargetComponent::OnInworldConversationTargetInitialized.AddUObject(
			this, &UInworldCharacterSubsystem::AddConversationTarget);
	ConversationTargetUnregisterDelegateHandle = UInworldConversationTargetComponent::OnInworldConversationTargetEndPlay.AddUObject(
		this, &UInworldCharacterSubsystem::RemoveConversationTarget);
}

void UInworldCharacterSubsystem::Deinitialize()
{
	UInworldConversationTargetComponent::OnInworldConversationTargetInitialized.Remove(ConversationTargetRegisterDelegateHandle);
	UInworldConversationTargetComponent::OnInworldConversationTargetEndPlay.Remove(ConversationTargetUnregisterDelegateHandle);

	Super::Deinitialize();
}

void UInworldCharacterSubsystem::AddConversationTarget(UInworldConversationTargetComponent* ConversationTarget)
{
	if (ConversationTargetMap.Contains(ConversationTarget->GetId()))
	{
		UE_LOG(LogInworldCharacter, Warning,
			TEXT("Attempted to add conversation target that is already registered with id:"
				 "%s."),
			*ConversationTarget->GetId());
		return;
	}

	if (ConversationTarget->IsA(UInworldConversationGroupComponent::StaticClass()))
	{
		UInworldConversationGroupComponent* ConversationGroupComponent =
			Cast<UInworldConversationGroupComponent>(ConversationTarget);

		ConversationGroupDelegateMap.Add(
			ConversationGroupComponent->GetId() + "_Added", ConversationGroupComponent->OnCharacterAddedNative.AddUObject(
																this, &UInworldCharacterSubsystem::OnCharacterAddedToConversation));
		ConversationGroupDelegateMap.Add(
			ConversationGroupComponent->GetId() + "_Removed", ConversationGroupComponent->OnCharacterRemovedNative.AddUObject(this,
																  &UInworldCharacterSubsystem::OnCharacterRemovedFromConversation));
	}

	ConversationTargetMap.Add(ConversationTarget->GetId(), ConversationTarget);

	TArray<UInworldConversationTargetComponent*> ConversationTargets;
	ConversationTargetMap.GenerateValueArray(ConversationTargets);
	OnConversationTargetListUpdated.Broadcast(ConversationTargets);
	OnConversationTargetListUpdatedNative.Broadcast(ConversationTargets);
}

void UInworldCharacterSubsystem::RemoveConversationTarget(UInworldConversationTargetComponent* ConversationTarget)
{
	if (!ConversationTargetMap.Contains(ConversationTarget->GetId()))
	{
		UE_LOG(LogInworldCharacter, Warning,
			TEXT("Attempted to remove conversation target with id (%s) that is not "
				 "registered."),
			*ConversationTarget->GetId());
		return;
	}

	if (ConversationTarget->IsA(UInworldConversationGroupComponent::StaticClass()))
	{
		UInworldConversationGroupComponent* ConversationGroupComponent =
			Cast<UInworldConversationGroupComponent>(ConversationTarget);
		ConversationGroupComponent->OnCharacterAddedNative.Remove(
			ConversationGroupDelegateMap.FindRef(ConversationGroupComponent->GetId() + "_Added"));
		ConversationGroupComponent->OnCharacterRemovedNative.Remove(
			ConversationGroupDelegateMap.FindRef(ConversationGroupComponent->GetId() + "_Removed"));

		ConversationGroupDelegateMap.Remove(ConversationGroupComponent->GetId() + "_Added");
		ConversationGroupDelegateMap.Remove(ConversationGroupComponent->GetId() + "_Removed");
	}

	ConversationTargetMap.Remove(ConversationTarget->GetId());

	TArray<UInworldConversationTargetComponent*> ConversationTargets;
	ConversationTargetMap.GenerateValueArray(ConversationTargets);
	OnConversationTargetListUpdated.Broadcast(ConversationTargets);
	OnConversationTargetListUpdatedNative.Broadcast(ConversationTargets);
}

void UInworldCharacterSubsystem::OnCharacterAddedToConversation(
	UInworldConversationGroup* ConversationGroup, UInworldCharacterComponent* InworldCharacter)
{
	if (ConversationTargetMap.Contains(InworldCharacter->GetId()))
	{
		RemoveConversationTarget(InworldCharacter);
	}
}

void UInworldCharacterSubsystem::OnCharacterRemovedFromConversation(
	UInworldConversationGroup* ConversationGroup, UInworldCharacterComponent* InworldCharacter)
{
	if (!ConversationTargetMap.Contains(InworldCharacter->GetId()))
	{
		AddConversationTarget(InworldCharacter);
	}
}

UInworldConversationTargetComponent* UInworldCharacterSubsystem::GetConversationTarget(const FString& Id)
{
	return ConversationTargetMap.FindRef(Id);
}

UInworldConversationTargetComponent* UInworldCharacterSubsystem::GetClosestConversationTarget(
	const FTransform& Transform, float MaxDistance, float ToleranceAngle) const
{
	float BestDist = MaxDistance;
	UInworldConversationTargetComponent* BestComponent = nullptr;
	TArray<FString> TargetIds;
	ConversationTargetMap.GetKeys(TargetIds);
	for (const FString& Id : TargetIds)
	{
		UInworldConversationTargetComponent* ConversationTargetComponent = ConversationTargetMap.FindRef(Id);
		if (IsValid(ConversationTargetComponent) && IsValid(ConversationTargetComponent->GetOwner()))
		{
			AActor* Owner = ConversationTargetComponent->GetOwner();
			if (MeasureDistanceAngle(Transform, Owner->GetActorTransform(), BestDist, ToleranceAngle))
			{
				BestComponent = ConversationTargetComponent;
			}
		}
	}

	return BestComponent;
}

UInworldConversationTargetComponent* UInworldCharacterSubsystem::GetClosestConversationTargetToConversationTarget(
	UInworldConversationTargetComponent* TargetConversationTarget, float MaxDistance, float ToleranceAngle) const
{
	float BestDist = MaxDistance;
	UInworldConversationTargetComponent* BestComponent = nullptr;
	if (IsValid(TargetConversationTarget) && IsValid(TargetConversationTarget->GetOwner()))
	{
		TArray<FString> TargetIds;
		ConversationTargetMap.GetKeys(TargetIds);
		for (const FString& Id : TargetIds)
		{
			UInworldConversationTargetComponent* ConversationTargetComponent = ConversationTargetMap.FindRef(Id);
			if ((ConversationTargetComponent == TargetConversationTarget) || (!IsValid(ConversationTargetComponent->GetOwner())))
			{
				continue;
			}

			if (MeasureDistanceAngle(TargetConversationTarget->GetOwner()->GetActorTransform(),
					ConversationTargetComponent->GetOwner()->GetActorTransform(), BestDist, ToleranceAngle))
			{
				BestComponent = ConversationTargetComponent;
			}
		}
	}
	return BestComponent;
}

bool UInworldCharacterSubsystem::GetCharacterData(
	UDataTable* DataTableToSearch, const FName& CharacterID, FInworldCharacterConfig& OutData) const
{
	if (!DataTableToSearch)
	{
		UE_LOG(LogInworldCharacter, Warning, TEXT("UInworldCharacterSubsystem::GetCharacterData - DataTableToSearch was null."));
		return false;
	}

	FInworldCharacterConfig* Row =
		DataTableToSearch->FindRow<FInworldCharacterConfig>(CharacterID, TEXT("UInworldCharacterSubsystem::GetCharacterData"));
	if (Row)
	{
		OutData = *Row;
		return true;
	}
	else
	{
		UE_LOG(LogInworldCharacter, Warning,
			TEXT("UInworldCharacterSubsystem::GetCharacterData - Row for CharacterID '%s' was not found in DataTable '%s'."),
			*CharacterID.ToString(), *DataTableToSearch->GetName());
	}

	return false;
}

bool UInworldCharacterSubsystem::MeasureDistanceAngle(
	const FTransform& TransformA, const FTransform& TransformB, float& MaxDistance, float ToleranceAngle)
{
	bool bIsInsideAngle = true;
	float CurrentAngle = UE_MAX_FLT;
	if (ToleranceAngle < 360.f)
	{
		// Make sure that each transform is facing each other
		// FVector AtoBDir = (TransformB.GetLocation() - TransformA.GetLocation());
		// float ADotResult = TransformA.GetForwardVector() | AtoBDir;
		// float BDotResult = TransformB.GetForwardVector() | (-1.f * AtoBDir);
		// bIsInsideAngle = ((ADotResult > 0.f) && (BDotResult > 0.f));

		const float YawA = static_cast<float>(FMath::DegreesToRadians(TransformA.Rotator().Yaw));
		const float YawB = static_cast<float>(FMath::DegreesToRadians(TransformB.Rotator().Yaw));

		// check if they're within tolerance angle.
		bIsInsideAngle = FMath::Abs(FMath::FindDeltaAngleRadians(YawA, YawB)) < FMath::DegreesToRadians(ToleranceAngle);
	}

	float CurrentDistance = (TransformA.GetLocation() - TransformB.GetLocation()).Size();
	bool bIsInsideDistance = false;
	if ((CurrentDistance <= MaxDistance) && bIsInsideAngle)
	{
		MaxDistance = CurrentDistance;
		bIsInsideDistance = true;
	}

	return bIsInsideDistance && bIsInsideAngle;
}

UInworldConversationTargetComponent* UInworldCharacterSubsystem::GetClosestConversationTargetToReticle(
	APlayerController* PlayerController, float MaxDistance)
{
	int32 ViewportSizeX;
	int32 ViewportSizeY;
	PlayerController->GetViewportSize(ViewportSizeX, ViewportSizeY);
	const FVector2D ScreenCenter(ViewportSizeX * 0.5f, ViewportSizeY * 0.5f);

	float BestDist = UE_MAX_FLT;
	UInworldConversationTargetComponent* BestConversationTargetComponent(nullptr);
	TArray<FString> TargetIds;
	ConversationTargetMap.GetKeys(TargetIds);
	for (const FString& Id : TargetIds)
	{
		UInworldConversationTargetComponent* ConversationTargetComponent = ConversationTargetMap.FindRef(Id);
		if (IsValid(ConversationTargetComponent) && IsValid(ConversationTargetComponent->GetOwner()))
		{
			AActor* Owner = ConversationTargetComponent->GetOwner();
			FVector ProjectedScreenLocation(0.f);
			if (PlayerController->ProjectWorldLocationToScreenWithDistance(Owner->GetActorLocation(), ProjectedScreenLocation))
			{
				if (ProjectedScreenLocation.Z <= MaxDistance)
				{
					FVector2D ScreenCoord(ProjectedScreenLocation.X, ProjectedScreenLocation.Y);
					float ScreenDist = (ScreenCoord - ScreenCenter).Size();
					if (ScreenDist < BestDist)
					{
						BestDist = ScreenDist;
						BestConversationTargetComponent = ConversationTargetComponent;
					}
				}
			}
		}
	}

	return BestConversationTargetComponent;
}

const TMap<FString, UInworldConversationTargetComponent*>& UInworldCharacterSubsystem::GetConversationTargetMap() const
{
	return ConversationTargetMap;
}
