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

#include "Components/InworldBaseCharacterComponent.h"

#include "Components/InworldSimplePlayerComponent.h"
#include "InworldBlueprintFunctionLibrary.h"
#include "InworldCharacter.h"
#include "InworldCharacterSubsystem.h"
#include "InworldRuntimeSettings.h"

#if WITH_EDITOR
#include "Misc/MessageDialog.h"
#endif

UInworldBaseCharacterComponent::UInworldBaseCharacterComponent(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	const UInworldRuntimeSettings* Settings = GetDefault<UInworldRuntimeSettings>();
	if (Settings)
	{
		CharacterDataTable = Settings->DefaultCharacterDataTable;
	}
}

void UInworldBaseCharacterComponent::Interrupt_Implementation()
{
	MessageQueue.Empty();
	CancelCurrentMessage();
	if (InworldCharacterAudioComponent.IsValid())
	{
		InworldCharacterAudioComponent->Interrupt();
	}
}

void UInworldBaseCharacterComponent::TrySendNextMessage()
{
	if (!bIsReady || MessageQueue.IsEmpty() || CurrentMessage.IsValid())
	{
		return;
	}

	MessageQueue.Dequeue(CurrentMessage);
	TMap<FName, FInworldDataHandle> RuntimeData;
	GetRuntimeData(RuntimeData);
	for (auto& KeyValuePair : CurrentMessage.RuntimeData)
	{
		RuntimeData.Emplace(KeyValuePair.Key, KeyValuePair.Value);
	}

	CurrentMessage.TimeSent = FDateTime::Now();
	CurrentMessage.ExecutionId = DialogueGenerationGraph->Execute(this, CurrentMessage.DataHandle, RuntimeData,
		FOnGraphResultNative::CreateWeakLambda(this,
			[this](const FString& ExecutionId, const FString& NodeId, const FInworldDataHandle& Data)
			{
				OnDialogueGenerationResult(ExecutionId, NodeId, Data);
			}),
		FOnGraphStatusNative::CreateWeakLambda(this,
			[this](const FString& ExecutionId, EInworldGraphExecutionStatus Status)
			{
				OnDialogueGenerationStatus(ExecutionId, Status);
			}));
}

void UInworldBaseCharacterComponent::OnDialogueGenerationStatus_Implementation(
	const FString& ExecutionId, EInworldGraphExecutionStatus Status)
{
	if (Status == EInworldGraphExecutionStatus::Completed || Status == EInworldGraphExecutionStatus::Failed ||
		Status == EInworldGraphExecutionStatus::Canceled)
	{
		CurrentMessage.Reset();
		TrySendNextMessage();
	}
}

FString UInworldBaseCharacterComponent::SendMessage_Implementation(const FInworldDataHandle& Message)
{
	return Execute_SendDataMessage(this, Message, {{PlayerProfileRuntimeDataKey, FInworldData_Struct(FInworldPlayerProfile())}});
}

FString UInworldBaseCharacterComponent::SendPlayerMessage_Implementation(
	const FInworldDataHandle& Message, const FInworldPlayerProfile& PlayerProfile)
{
	FString MessageId = Execute_SendDataMessage(this, Message, {{PlayerProfileRuntimeDataKey, FInworldData_Struct(PlayerProfile)}});
	OnPlayerMessage.Broadcast(this, PlayerProfile, MessageId, Message);
	OnPlayerMessageNative.Broadcast(this, PlayerProfile, MessageId, Message);
	return MessageId;
}

FString UInworldBaseCharacterComponent::SendDataMessage_Implementation(
	const FInworldDataHandle& Message, const TMap<FName, FInworldDataHandle>& RuntimeData)
{
	FString MessageId = FGuid::NewGuid().ToString();
	FInworldCharacterMessage CharacterMessage;
	CharacterMessage.MessageId = MessageId;
	CharacterMessage.DataHandle = Message;
	CharacterMessage.RuntimeData = RuntimeData;
	MessageQueue.Enqueue(CharacterMessage);
	TrySendNextMessage();
	return MessageId;
}

bool UInworldBaseCharacterComponent::UpdateCharacterFromDataTable(
	const TSoftObjectPtr<UDataTable>& NewCharacterDataTableRef, FName NewCharacterID)
{
	const UInworldCharacterSubsystem* Subsystem = GetDefault<UInworldCharacterSubsystem>();
	if (!Subsystem)
	{
		return false;
	}

	if (NewCharacterID == NAME_None)
	{
		UE_LOG(LogInworldCharacter, Warning, TEXT("UpdateCharacterFromDataTable: Provided CharacterID is None."));
		return false;
	}

	UDataTable* ResolvedDataTable = NewCharacterDataTableRef.LoadSynchronous();
	if (!ResolvedDataTable)
	{
		UE_LOG(LogInworldCharacter, Warning, TEXT("UpdateCharacterFromDataTable: Failed to load DataTable asset '%s'."),
			*NewCharacterDataTableRef.ToString());
		return false;
	}

	FInworldCharacterConfig CharacterData;
	if (Subsystem->GetCharacterData(ResolvedDataTable, NewCharacterID, CharacterData))
	{
		ApplyCharacterData(CharacterData);

		this->bUseDataTable = true;
		this->CharacterDataTable = NewCharacterDataTableRef;
		this->CharacterID = NewCharacterID;

		UE_LOG(LogInworldCharacter, Log, TEXT("Successfully updated character '%s' using DataTable '%s', Row '%s'."),
			*GetOwner()->GetName(), *NewCharacterDataTableRef->GetName(), *NewCharacterID.ToString());
		return true;
	}
	else
	{
		UE_LOG(LogInworldCharacter, Warning,
			TEXT("UpdateCharacterFromDataTable: Failed to get data for CharacterID '%s' from DataTable '%s'."),
			*NewCharacterID.ToString(), *NewCharacterDataTableRef->GetName());
		return false;
	}
}

#if WITH_EDITOR
void UInworldBaseCharacterComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
	Super::PostEditChangeProperty(PropertyChangedEvent);

	const FName PropertyName = (PropertyChangedEvent.Property != nullptr) ? PropertyChangedEvent.Property->GetFName() : NAME_None;

	if (PropertyName == GET_MEMBER_NAME_CHECKED(UInworldBaseCharacterComponent, CharacterID))
	{
		ValidateCharacterIDProperty();
	}
}

void UInworldBaseCharacterComponent::ValidateCharacterIDProperty()
{
	if (!bUseDataTable || CharacterID == NAME_None)
	{
		return;
	}

	UDataTable* ResolvedDataTable = CharacterDataTable.LoadSynchronous();
	if (!ResolvedDataTable)
	{
		return;
	}

	const UInworldCharacterSubsystem* Subsystem = GetDefault<UInworldCharacterSubsystem>();
	if (!Subsystem)
	{
		return;
	}

	FInworldCharacterConfig CharacterData;
	if (!Subsystem->GetCharacterData(ResolvedDataTable, CharacterID, CharacterData))
	{
		return;
	}

	const EInworldComponentType ExpectedType = GetComponentType();
	if (CharacterData.ComponentType == ExpectedType)
	{
		return;
	}

	const FText DialogTitle = NSLOCTEXT("Inworld", "ComponentTypeMismatchTitle", "Component Type Mismatch");
	const FText DialogText = FText::Format(
		NSLOCTEXT("Inworld", "ComponentTypeMismatchWarning",
			"The selected Character ID '{0}' is for a '{1}', but this is a '{2}'.\n\nSome data may not be used. Continue?"),
		FText::FromName(CharacterID),
		StaticEnum<EInworldComponentType>()->GetDisplayNameTextByValue(static_cast<int64>(CharacterData.ComponentType)),
		StaticEnum<EInworldComponentType>()->GetDisplayNameTextByValue(static_cast<int64>(ExpectedType)));

	const EAppReturnType::Type Result = FMessageDialog::Open(EAppMsgType::OkCancel, DialogText, DialogTitle);

	if (Result == EAppReturnType::Cancel)
	{
		CharacterID = NAME_None;

		FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
		PropertyModule.NotifyCustomizationModuleChanged();
	}
}
#endif

void UInworldBaseCharacterComponent::InitializeAudioCallbacks()
{
	UActorComponent* ActorComponent = GetOwner()->GetComponentByClass(UInworldVoiceAudioComponent::StaticClass());
	InworldCharacterAudioComponent = Cast<UInworldVoiceAudioComponent>(ActorComponent);
	if (!InworldCharacterAudioComponent.IsValid())
	{
		UE_LOG(LogInworldCharacter, Warning,
			TEXT("UInworldBaseCharacterComponent::InitializeAudioCallbacks - no "
				 "Inworld Voice Audio Component found."))
		return;
	}

	InworldCharacterAudioComponent->OnVoiceAudioStartNative.AddWeakLambda(this,
		[this](UInworldVoiceAudioComponent* AudioComponent, const FInworldData_TTSOutput& TTSOutputData)
		{
			CurrentVisemeData = TTSOutputData.VisemeInfos;
			OnUtteranceStart.Broadcast(this, TTSOutputData.Text);
			OnUtteranceStartNative.Broadcast(this, TTSOutputData.Text);
		});

	InworldCharacterAudioComponent->OnVoiceAudioPlaybackNative.AddWeakLambda(this,
		[this](UInworldVoiceAudioComponent* AudioComponent, float PlaybackDuration, float PlaybackTimeSeconds)
		{
			FInworldVisemeBlends VisemeBlends =
				UInworldBlueprintFunctionLibrary::GetVisemeBlends(PlaybackTimeSeconds, CurrentVisemeData);
			OnUtterancePlayback.Broadcast(this, VisemeBlends, PlaybackDuration, PlaybackTimeSeconds);
			OnUtterancePlaybackNative.Broadcast(this, VisemeBlends, PlaybackDuration, PlaybackTimeSeconds);
		});

	InworldCharacterAudioComponent->OnVoiceAudioInterruptNative.AddWeakLambda(this,
		[this](UInworldVoiceAudioComponent* AudioComponent, const FInworldData_TTSOutput& TTSOutputData)
		{
			OnUtteranceInterrupt.Broadcast(this, TTSOutputData.Text);
			OnUtteranceInterruptNative.Broadcast(this, TTSOutputData.Text);

			OnUtterancePlayback.Broadcast(this, {}, 0, 0);
			OnUtterancePlaybackNative.Broadcast(this, {}, 0, 0);
		});

	InworldCharacterAudioComponent->OnVoiceAudioCompleteNative.AddWeakLambda(this,
		[this](UInworldVoiceAudioComponent* AudioComponent, const FInworldData_TTSOutput& TTSOutputData)
		{
			OnUtteranceComplete.Broadcast(this, TTSOutputData.Text);
			OnUtteranceCompleteNative.Broadcast(this, TTSOutputData.Text);
		});
}

void UInworldBaseCharacterComponent::InitializeFromDataTable()
{
	const UInworldCharacterSubsystem* Subsystem = GetDefault<UInworldCharacterSubsystem>();
	if (!Subsystem)
	{
		return;
	}

	UDataTable* ResolvedDataTable = CharacterDataTable.LoadSynchronous();
	if (!ResolvedDataTable)
	{
		UE_LOG(LogInworldCharacter, Warning,
			TEXT("UInworldBaseCharacterComponent::InitializeFromDataTable - CharacterDataTable is not set or failed to load for "
				 "component on actor '%s'."),
			*GetOwner()->GetName());
		return;
	}

	FInworldCharacterConfig CharacterData;
	if (Subsystem->GetCharacterData(ResolvedDataTable, CharacterID, CharacterData))
	{
		ApplyCharacterData(CharacterData);
	}
	else
	{
		UE_LOG(LogInworldCharacter, Warning,
			TEXT("UInworldBaseCharacterComponent::InitializeFromDataTable - Failed to find CharacterID '%s' in DataTable '%s'."),
			*CharacterID.ToString(), *ResolvedDataTable->GetName());
	}
}

void UInworldBaseCharacterComponent::BeginPlay()
{
	InitializeAudioCallbacks();
	Super::BeginPlay();

	if (bUseDataTable)
	{
		InitializeFromDataTable();
	}

	if (IsValid(DialogueGenerationGraphAsset))
	{
		DialogueGenerationGraphAsset->GetGraphInstance(FOnGraphCompiledNative::CreateWeakLambda(this,
			[this](UInworldGraph* Graph, bool bSuccess)
			{
				if (bSuccess)
				{
					DialogueGenerationGraph = Cast<UInworldGraph>(Graph);
					TrySignalReady();
				}
				else
				{
					UE_LOG(LogInworldCharacter, Error,
						TEXT("UInworldBasePlayerComponent::BeginPlay - Failed to get Dialogue Generation graph."));
				}
			}));
	}
}

void UInworldBaseCharacterComponent::TrySignalReady()
{
	if (bIsReady)
	{
		return;
	}

	if (DialogueGenerationGraph != nullptr)
	{
		Super::TrySignalReady();
		TrySendNextMessage();
	}
}

void UInworldBaseCharacterComponent::CancelCurrentMessage()
{
	if (CurrentMessage.IsValid())
	{
		DialogueGenerationGraph->CancelExecution(CurrentMessage.ExecutionId);
		CurrentMessage.Reset();
	}
}
