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

#pragma once

#include "CoreMinimal.h"
#include "Graph/InworldData.h"
#include "Graph/InworldProcessContext.h"
#include "InworldRuntimeTypes.h"
#include "JsonObjectConverter.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Templates/Casts.h"
#include "UObject/UnrealType.h"

#include "InworldBlueprintFunctionLibrary.generated.h"

/**
 * @enum EJinjaArgumentType
 * @brief Enumeration of supported data types for Jinja template arguments.
 */
UENUM(BlueprintType)
enum class EJinjaArgumentType : uint8
{
	/** Boolean value type */
	BOOL UMETA(DisplayName = "Boolean"),
	/** Floating point value type */
	FLOAT UMETA(DisplayName = "Float"),
	/** Integer value type */
	INT UMETA(DisplayName = "Integer"),
	/** String value type */
	STRING UMETA(DisplayName = "String"),
	/** Structure value type */
	STRUCT UMETA(DisplayName = "Struct"),
	/** Unknown or uninitialized type */
	UNKNOWN UMETA(Hidden),
};

/**
 * @struct FJinjaArgument
 * @brief Structure for holding typed arguments for Jinja template rendering.
 *
 * This structure provides a type-safe way to pass various data types as arguments
 * to Jinja template rendering functions. It supports primitive types (bool, float, int, string)
 * as well as structured data through JSON object conversion.
 */
USTRUCT(BlueprintType)
struct FJinjaArgument
{
	GENERATED_BODY()

	FJinjaArgument() = default;

	FJinjaArgument(const UScriptStruct* ScriptStruct, const void* Data)
	{
		ArgType = EJinjaArgumentType::STRUCT;
		JsonObject = MakeShareable(new FJsonObject());
		FJsonObjectConverter::UStructToJsonObject(ScriptStruct, Data, JsonObject);
	}

	template <typename T>
	FJinjaArgument(const T& Val) : FJinjaArgument(T::StaticStruct(), &Val)
	{
	}

#define DECLARE_JINJA_ARGUMENT(Name, Type, Enum) \
	template <>                                  \
	FJinjaArgument(const Type& Val)              \
	{                                            \
		ArgType = Enum;                          \
		Name = Val;                              \
	}
	DECLARE_JINJA_ARGUMENT(Bool, bool, EJinjaArgumentType::BOOL)
	DECLARE_JINJA_ARGUMENT(Float, float, EJinjaArgumentType::FLOAT)
	DECLARE_JINJA_ARGUMENT(Int, int32, EJinjaArgumentType::INT)
	DECLARE_JINJA_ARGUMENT(String, FString, EJinjaArgumentType::STRING)
#undef DECLARE_JINJA_ARGUMENT

	void Switch(TFunction<void(bool)> BoolFn, TFunction<void(float)> FloatFn, TFunction<void(int32)> IntFn,
		TFunction<void(const FString&)> StringFn, TFunction<void(TSharedPtr<FJsonObject>)> StructFn) const;

private:
	EJinjaArgumentType ArgType{EJinjaArgumentType::UNKNOWN};
	bool Bool{false};
	float Float{0.f};
	int32 Int{0};
	FString String{""};
	TSharedRef<FJsonObject> JsonObject{MakeShareable(new FJsonObject())};
};

UCLASS()
class INWORLDRUNTIME_API UInworldBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintPure, CustomThunk, Category = "Inworld|Data",
		meta = (CustomStructureParam = "Data", BlueprintInternalUseOnly = "true"))
	static void UnwrapInworldDataHandle(
		const FInworldDataHandle& DataHandle, UScriptStruct* ScriptStruct, int32& Data, bool& UnwrapSuccessful);

	DECLARE_FUNCTION(execUnwrapInworldDataHandle);

	UFUNCTION(BlueprintPure, CustomThunk, Category = "Inworld|Data",
		meta = (CustomStructureParam = "Data", BlueprintInternalUseOnly = "true"))
	static FInworldDataHandle WrapInworldData(UScriptStruct* ScriptStruct, const int32& Data);

	DECLARE_FUNCTION(execWrapInworldData);

	UFUNCTION(BlueprintPure, Customthunk, Category = "Inworld|Data",
		meta = (CustomStructureParam = "Data", BlueprintInternalUseOnly = "true"))
	static void CreateInworldDataError(UScriptStruct* InworldDataStructType, const FString& Reason, int32& Data);

	DECLARE_FUNCTION(execCreateInworldDataError);

	UFUNCTION(BlueprintPure, CustomThunk, Category = "Inworld|ProcessContext",
		meta = (CustomStructureParam = "Struct", BlueprintInternalUseOnly = "true"))
	static void GetRuntimeStruct(
		UInworldProcessContext* ProcessContext, const FName& Key, UScriptStruct* ScriptStruct, int32& Struct, bool& Successful);

	DECLARE_FUNCTION(execGetRuntimeStruct);

	/**
	 * Removes leading and trailing quotes as well as getting rid of double spaces and other artifacts of LLM text generation.
	 *
	 * @param LLMOutputString The LLM output string to edit
	 * @param OutTrimmedLLMOutputString The result string with spaces and quotes removed
	 */
	UFUNCTION(BlueprintCallable, Category = "Inworld|Data")
	static void TrimLLMOutput(const FString& LLMOutputString, FString& OutTrimmedLLMOutputString);

	UFUNCTION(BlueprintPure, Category = "Jinja", meta = (DisplayName = "Render Jinja (Json)"))
	static UPARAM(DisplayName = "Success") bool RenderJinjaWithJson(
		const FString& Template, const FString& JsonString, FString& OutJinja);

	UFUNCTION(BlueprintPure, Category = "Jinja", meta = (CompactNodeTitle = "->", BlueprintAutocast))
	static FJinjaArgument CreateBooleanJinjaArgument(bool Value);

	UFUNCTION(BlueprintPure, Category = "Jinja", meta = (CompactNodeTitle = "->", BlueprintAutocast))
	static FJinjaArgument CreateFloatJinjaArgument(float Value);

	UFUNCTION(BlueprintPure, Category = "Jinja", meta = (CompactNodeTitle = "->", BlueprintAutocast))
	static FJinjaArgument CreateIntJinjaArgument(int32 Value);

	UFUNCTION(BlueprintPure, Category = "Jinja", meta = (CompactNodeTitle = "->", BlueprintAutocast))
	static FJinjaArgument CreateStringJinjaArgument(FString Value);

	UFUNCTION(BlueprintPure, CustomThunk, BlueprintInternalUseOnly, Category = "Jinja",
		meta = (CustomStructureParam = "Data", AutoCreateRefTerm = "Data"))
	static FJinjaArgument CreateStructJinjaArgument(const int32& Data);
	DECLARE_FUNCTION(execCreateStructJinjaArgument);

	UFUNCTION(BlueprintPure, Category = "Jinja", meta = (DisplayName = "Render Jinja (Arg Map)"))
	static UPARAM(DisplayName = "Success") bool RenderJinjaWithArgMap(
		const FString& Template, const TMap<FString, FJinjaArgument>& ArgMap, FString& OutJinja);

	/** Calculates viseme blend weights for a given playback time based on VisemeInfos.
	 * Smoothly interpolates between visemes.
	 *
	 * @param PlaybackTimeSeconds The current playback in seconds.
	 * @param VisemeInfos Array of visem infos sorted by timestamps.
	 */
	UFUNCTION(BlueprintCallable, Category = "Inworld|Viseme")
	static FInworldVisemeBlends GetVisemeBlends(const float PlaybackTimeSeconds, const TArray<FInworldVisemeInfo>& VisemeInfos);

	static FInworldDataHandle ErrorOnDestroying();

	UFUNCTION(BlueprintPure, Category = "Inworld|Data")
	static void BreakAudio(const FInworldData_Audio& Audio, TArray<uint8>& OutWaveform, int32& OutSampleRate)
	{
		OutWaveform = *Audio.Waveform;
		OutSampleRate = Audio.SampleRate;
	}

	UFUNCTION(BlueprintPure, Category = "Inworld|Data")
	static FInworldData_Audio MakeAudio(const TArray<uint8>& Waveform, int32 SampleRate)
	{
		FInworldData_Audio Audio;
		Audio.Waveform = MakeShared<TArray<uint8>>(Waveform);
		Audio.SampleRate = SampleRate;
		return Audio;
	}

	UFUNCTION(BlueprintPure, Category = "Inworld|Data", meta = (DisplayName = "HasNext"))
	static bool DataStreamTTSOutputHasNext(const FInworldData_DataStream_TTSOutput& TTSOutputStream);

	UFUNCTION(BlueprintCallable, Category = "Inworld|Data", meta = (DisplayName = "GetNext", ReturnDisplayName = "TTSOutput"))
	static FInworldDataHandle DataStreamTTSOutputGetNext(FInworldData_DataStream_TTSOutput TTSOutputStream);

#if WITH_EDITOR
	UFUNCTION(BlueprintInternalUseOnly)
	static bool Prototype_CheckEdgeCondition(const FInworldDataHandle& Data)
	{
		return false;
	}
	UFUNCTION(BlueprintInternalUseOnly)
	static FInworldDataHandle Prototype_NodeProcess(
		const UInworldProcessContext* ProcessContext, const TArray<FInworldDataHandle>& Data)
	{
		return FInworldDataHandle{};
	}
#endif
};
