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

#include "CoreMinimal.h"
#include "Graph/Assets/InworldNodeAsset.h"
#include "InworldRuntimeEditorSettings.h"
#include "Graph/InworldProcessContext.h"
#include "Graph/Nodes/InworldNode_GetRuntimeData.h"

#include "InworldGraphNodeInputOutputSlot.generated.h"

USTRUCT()
struct INWORLDRUNTIMEEDITOR_API FInworldNodeInputOutputSlot
{
	GENERATED_BODY()

	UPROPERTY(EditDefaultsOnly, Category = "Inworld")
	FString Name;

	UPROPERTY(EditDefaultsOnly, Category = "Inworld")
	TArray<UStruct*> ValidTypes;

	UPROPERTY(EditDefaultsOnly, Category = "Inworld")
	bool bOptional{false};

	bool operator==(const FInworldNodeInputOutputSlot& Other) const
	{
		return (Name.Equals(Other.Name) && bOptional == Other.bOptional && ValidTypes == Other.ValidTypes);
	}

	bool operator!=(const FInworldNodeInputOutputSlot& Other) const
	{
		return !(*this == Other);
	}

	UStruct* GetFirstType() const
	{
		return ValidTypes.Num() > 0 ? ValidTypes[0] : nullptr;
	}

	FInworldNodeInputOutputSlot()
	{
	}
	FInworldNodeInputOutputSlot(const FString& InName, const bool InbOptional = false) : Name(InName), bOptional(InbOptional)
	{
	}
	FInworldNodeInputOutputSlot(const FString& InName, UStruct* InType, const bool InbOptional = false)
		: Name(InName), bOptional(InbOptional)
	{
		ValidTypes.Add(InType);
	}
	FInworldNodeInputOutputSlot(const FString& InName, const TArray<UStruct*>& InTypes, const bool InbOptional = false)
		: Name(InName), bOptional(InbOptional)
	{
		ValidTypes.Append(InTypes);
	}

	bool AreCompatibleWith(const FInworldNodeInputOutputSlot& Other) const
	{
		const UStruct* Wildcard = FInworldDataHandle::StaticStruct();
		const bool bInHasWildcard = Other.ValidTypes.Contains(const_cast<UStruct*>(Wildcard));
		const bool bOutHasWildcard = ValidTypes.Contains(const_cast<UStruct*>(Wildcard));
		if (bInHasWildcard || bOutHasWildcard || ValidTypes.Num() == 0 || Other.ValidTypes.Num() == 0)
		{
			return true;
		}
		for (UStruct* OtherType : Other.ValidTypes)
		{
			if (ValidTypes.Contains(OtherType))
			{
				return true;
			}
		}
		return false;
	}

	FString TypesToString() const
	{
		const bool bFriendlyLabels = GetDefault<UInworldRuntimeEditorSettings>()->bUseFriendlyNamesForDataTypes;
		FString Result;
		for (int32 i = 0; i < ValidTypes.Num(); ++i)
		{
			if (ValidTypes[i])
			{
				FString DataTypeFriendly = ValidTypes[i]->GetName();
				if (bFriendlyLabels)
				{
					DataTypeFriendly.RemoveFromStart("InworldData_");
				}
				Result += DataTypeFriendly;
			}
			else
			{
				Result += TEXT("None");
			}
			if (i < ValidTypes.Num() - 1)
			{
				Result += TEXT(", ");
			}
		}
		return Result;
	};

	FString GetDescription() const
	{
		FString Result;
		const bool bFriendlyLabels = GetDefault<UInworldRuntimeEditorSettings>()->bUseFriendlyNamesForDataTypes;
		if (!ValidTypes.IsEmpty())
		{
			FString TypesString;
			for (int32 i = 0; i < ValidTypes.Num(); ++i)
			{
				if (ValidTypes[i])
				{
					FString ValidTypeFriendly = ValidTypes[i]->GetName();
					if (ValidTypeFriendly.Equals("InworldDataHandle"))
					{
						TypesString = TEXT("Any");
						break;
					}
					if (bFriendlyLabels)
					{
						ValidTypeFriendly.RemoveFromStart("InworldData_");
					}
					TypesString += ValidTypeFriendly;
					if (i < ValidTypes.Num() - 1)
					{
						TypesString += TEXT(" OR ");
					}
				}
			}
			Result += TypesString;
		}
		if (bOptional)
		{
			Result += TEXT("\nOptional");
		}
		return Result;
	}

	static TArray<FInworldNodeInputOutputSlot> GetInputOutputSlotsForNodeInput(const UInworldNode* Node)
	{
		TArray<FInworldNodeInputOutputSlot> Inputs;
		TArray<TArray<FString>> InputTypeNames;
		TArray<int32> InputCounts;
		for (const UFunction* ProcessFunction : Node->GetProcessFunctions())
		{
			int32 Index = 0;
			for (TFieldIterator<FProperty> PropIt(ProcessFunction); PropIt; ++PropIt)
			{
				if (FProperty* const Property = *PropIt; Property->HasAnyPropertyFlags(CPF_Parm))
				{
					if (!Property->HasAnyPropertyFlags(CPF_ReturnParm) &&
						!(Property->HasAnyPropertyFlags(CPF_OutParm) && !Property->HasAnyPropertyFlags(CPF_ConstParm)))
					{
						UStruct* Struct{nullptr};
						if (FStructProperty* const StructProperty = CastField<FStructProperty>(Property); StructProperty != nullptr)
						{
							Struct = StructProperty->Struct;
						}
						else if (FObjectPropertyBase* const ObjectProperty = CastField<FObjectProperty>(Property);
							ObjectProperty != nullptr && ObjectProperty->PropertyClass != UInworldProcessContext::StaticClass())
						{
							Struct = ObjectProperty->PropertyClass;
						}
						else if (FBoolProperty* const BoolProperty = CastField<FBoolProperty>(Property); BoolProperty != nullptr)
						{
							Struct = FInworldData_Bool::StaticStruct();
						}
						else if (FIntProperty* const IntProperty = CastField<FIntProperty>(Property); IntProperty != nullptr)
						{
							Struct = FInworldData_Int::StaticStruct();
						}
						else if (FDoubleProperty* const FloatProperty = CastField<FDoubleProperty>(Property);
							FloatProperty != nullptr)
						{
							Struct = FInworldData_Float::StaticStruct();
						}
						if (Struct != nullptr)
						{
							FInworldNodeInputOutputSlot& Input =
								Inputs.IsValidIndex(Index) ? Inputs[Index] : Inputs.Emplace_GetRef();
							TArray<FString>& InputTypeName =
								InputTypeNames.IsValidIndex(Index) ? InputTypeNames[Index] : InputTypeNames.Emplace_GetRef();
							int32& InputCount = InputCounts.IsValidIndex(Index) ? InputCounts[Index] : InputCounts.Emplace_GetRef();
							InputCount++;
							InputTypeName.AddUnique(Property->GetDisplayNameText().ToString());
							Inputs[Index].ValidTypes.AddUnique(Struct);
							Index++;
						}
					}
				}
			}
		}
		for (int32 i = 0; i < Inputs.Num(); ++i)
		{
			if (InputCounts[0] > InputCounts[i])
			{
				Inputs[i].bOptional = true;
			}
			Inputs[i].Name = FString::Join(InputTypeNames[i], TEXT("/"));
		}
		return Inputs;
	}

	static FORCEINLINE TArray<FInworldNodeInputOutputSlot> GetInputOutputSlotsForNodeInput(const UInworldNodeAsset* NodeAsset)
	{
		return NodeAsset != nullptr ? GetInputOutputSlotsForNodeInput(NodeAsset->RuntimeNode)
									: TArray<FInworldNodeInputOutputSlot>{};
	}

	static FInworldNodeInputOutputSlot GetInputOutputSlotForNodeOutput(const UInworldNode* Node)
	{
		if (Node == nullptr)
		{
			return {};
		}
		if (Node->IsA<UInworldNode_GetRuntimeData>())
		{

		}
		FInworldNodeInputOutputSlot Output;
		TArray<FString> OutputTypeNames{};
		for (const UFunction* ProcessFunction : Node->GetProcessFunctions())
		{
			for (TFieldIterator<FProperty> PropIt(ProcessFunction); PropIt; ++PropIt)
			{
				if (FProperty* const Property = *PropIt; Property->HasAnyPropertyFlags(CPF_Parm))
				{
					if (Property->HasAnyPropertyFlags(CPF_ReturnParm) ||
						(Property->HasAnyPropertyFlags(CPF_OutParm) && !Property->HasAnyPropertyFlags(CPF_ConstParm)))
					{
						UStruct* Struct{nullptr};
						FString DisplayName;
						if (Node->IsA<UInworldNode_GetRuntimeData>())
						{
							const UInworldNode_GetRuntimeData* GetRuntimeDataNode{Cast<UInworldNode_GetRuntimeData>(Node)};
							DisplayName = GetRuntimeDataNode->RuntimeDataKey.ToString();
							switch (GetRuntimeDataNode->DataType)
							{
								case EInworldGetRuntimeDataType::STRUCT:
									Struct = GetRuntimeDataNode->StructType;
									break;
								case EInworldGetRuntimeDataType::OBJECT:
									Struct = *GetRuntimeDataNode->ObjectType;
									break;
								case EInworldGetRuntimeDataType::BOOL:
									Struct = FInworldData_Bool::StaticStruct();
									break;
								case EInworldGetRuntimeDataType::FLOAT:
									Struct = FInworldData_Float::StaticStruct();
									break;
								case EInworldGetRuntimeDataType::INT:
									Struct = FInworldData_Int::StaticStruct();
									break;
							}
						}
						else if (FStructProperty* const StructProperty = CastField<FStructProperty>(Property); StructProperty != nullptr)
						{
							Struct = StructProperty->Struct;
						}
						else if (FObjectPropertyBase* const ObjectProperty = CastField<FObjectProperty>(Property);
							ObjectProperty != nullptr && ObjectProperty->PropertyClass != UInworldProcessContext::StaticClass())
						{
							Struct = ObjectProperty->PropertyClass;
						}
						else if (FBoolProperty* const BoolProperty = CastField<FBoolProperty>(Property); BoolProperty != nullptr)
						{
							Struct = FInworldData_Bool::StaticStruct();
						}
						else if (FIntProperty* const IntProperty = CastField<FIntProperty>(Property); IntProperty != nullptr)
						{
							Struct = FInworldData_Int::StaticStruct();
						}
						else if (FDoubleProperty* const FloatProperty = CastField<FDoubleProperty>(Property);
							FloatProperty != nullptr)
						{
							Struct = FInworldData_Float::StaticStruct();
						}
						if (Struct != nullptr)
						{
							OutputTypeNames.AddUnique(
								DisplayName.IsEmpty() ? Property->GetDisplayNameText().ToString() : DisplayName);
							Output.ValidTypes.AddUnique(Struct);
						}
					}
				}
			}
		}
		Output.Name = FString::Join(OutputTypeNames, TEXT("/"));
		return Output;
	}

	static FORCEINLINE FInworldNodeInputOutputSlot GetInputOutputSlotForNodeOutput(const UInworldNodeAsset* NodeAsset)
	{
		return NodeAsset != nullptr ? GetInputOutputSlotForNodeOutput(NodeAsset->RuntimeNode) : FInworldNodeInputOutputSlot{};
	}
};
