/*******************************************************************************
The content of this file includes portions of the proprietary AUDIOKINETIC Wwise
Technology released in source code form as part of the game integration package.
The content of this file may not be used without valid licenses to the
AUDIOKINETIC Wwise Technology.
Note that the use of the game engine is subject to the Unreal(R) Engine End User
License Agreement at https://www.unrealengine.com/en-US/eula/unreal
 
License Usage
 
Licensees holding valid licenses to the AUDIOKINETIC Wwise Technology may use
this file in accordance with the end user license agreement provided with the
software or, alternatively, in accordance with the terms contained
in a written agreement between you and Audiokinetic Inc.
Copyright (c) 2023 Audiokinetic Inc.
*******************************************************************************/

/*=============================================================================
	AkEvent.cpp:
=============================================================================*/

#include "AkAudioEvent.h"
#include "AkAudioBank.h"
#include "AkAudioDevice.h"
#include "AkComponent.h"
#include "AkComponentCallbackManager.h"
#include "AkGameObject.h"
#include "AkRoomComponent.h"
#include "AkUnrealHelper.h"
#include "Wwise/WwiseExternalSourceManager.h"
#include "Wwise/WwiseResourceLoader.h"
#include "Wwise/API/WwiseSoundEngineAPI.h"
#include "Wwise/Stats/Global.h"
#include "Wwise/Stats/AkAudio.h"

#include <inttypes.h>

#if WITH_EDITORONLY_DATA
#include "Wwise/WwiseProjectDatabase.h"
#include "Wwise/WwiseResourceCooker.h"
#endif

int32 UAkAudioEvent::PostOnActor(const AActor* Actor, const FOnAkPostEventCallback& Delegate, const int32 CallbackMask,
                                 const bool bStopWhenAttachedObjectDestroyed)
{
	return PostOnActor(Actor, &Delegate, nullptr, nullptr,
		AkCallbackTypeHelpers::GetCallbackMaskFromBlueprintMask(CallbackMask), nullptr, bStopWhenAttachedObjectDestroyed);
}

int32 UAkAudioEvent::PostOnComponent(UAkComponent* Component, const FOnAkPostEventCallback& Delegate,
	const int32 CallbackMask, const bool bStopWhenAttachedObjectDestroyed)
{
	return PostOnComponent(Component, &Delegate, nullptr, nullptr,
		AkCallbackTypeHelpers::GetCallbackMaskFromBlueprintMask(CallbackMask), nullptr, bStopWhenAttachedObjectDestroyed);
}

int32 UAkAudioEvent::PostOnGameObject(UAkGameObject* GameObject, const FOnAkPostEventCallback& Delegate, const int32 CallbackMask)
{
	return PostOnGameObject(GameObject, &Delegate, nullptr, nullptr,
		AkCallbackTypeHelpers::GetCallbackMaskFromBlueprintMask(CallbackMask), nullptr);
}

int32 UAkAudioEvent::PostOnActorAndWait(const AActor* Actor, const bool bStopWhenAttachedObjectDestroyed,
	const FLatentActionInfo LatentActionInfo)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostOnActorAndWait"));
	if (UNLIKELY(!IsValid(Actor) || Actor->IsActorBeingDestroyed()))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Failed to post latent AkAudioEvent '%s' with an actor that's not valid. The actor needs to be valid in order to wait for it."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	auto* World = Actor->GetWorld();
	if (UNLIKELY(!World))
	{
		UE_LOG(LogAkAudio, Log, TEXT("Failed to post latent AkAudioEvent '%s' with an actor '%s' world that's not valid."), *GetName(), *Actor->GetName());
		return AK_INVALID_PLAYING_ID;
	}

	FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
	FWaitEndOfEventAction* LatentAction = new FWaitEndOfEventAction(LatentActionInfo);
	LatentActionManager.AddNewAction(LatentActionInfo.CallbackTarget, LatentActionInfo.UUID, LatentAction);

	if (UNLIKELY(!World->AllowAudioPlayback()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' with an actor '%s' world '%s' that doesn't allow audio playback."), *GetName(), *Actor->GetName(), *World->GetName());
		LatentAction->EventFinished = true;
		return AK_INVALID_PLAYING_ID;
	}

	const auto PlayingID = PostOnActor(Actor, nullptr, nullptr, nullptr, (AkCallbackType)0, LatentAction, bStopWhenAttachedObjectDestroyed);
	if (UNLIKELY(PlayingID == AK_INVALID_PLAYING_ID))
	{
		LatentAction->EventFinished = true;
	}
	return PlayingID;
}

int32 UAkAudioEvent::PostOnComponentAndWait(UAkComponent* Component, const bool bStopWhenAttachedObjectDestroyed,
	const FLatentActionInfo LatentActionInfo)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostOnComponentAndWait"));
	if (UNLIKELY(!IsValid(Component)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Failed to post latent AkAudioEvent '%s' with a component that's not valid. The component needs to be valid in order to wait for it."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	auto* World = Component->GetWorld();
	if (UNLIKELY(!World))
	{
		UE_LOG(LogAkAudio, Log, TEXT("Failed to post latent AkAudioEvent '%s' with a component '%s' world that's not valid."), *GetName(), *Component->GetName());
		return AK_INVALID_PLAYING_ID;
	}

	FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
	FWaitEndOfEventAction* LatentAction = new FWaitEndOfEventAction(LatentActionInfo);
	LatentActionManager.AddNewAction(LatentActionInfo.CallbackTarget, LatentActionInfo.UUID, LatentAction);

	if (UNLIKELY(!World->AllowAudioPlayback()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' with a component '%s' world '%s' that doesn't allow audio playback."), *GetName(), *Component->GetName(), *World->GetName());
		LatentAction->EventFinished = true;
		return AK_INVALID_PLAYING_ID;
	}

	const auto PlayingID = PostOnComponent(Component, nullptr, nullptr, nullptr, (AkCallbackType)0, LatentAction, bStopWhenAttachedObjectDestroyed);
	if (UNLIKELY(PlayingID == AK_INVALID_PLAYING_ID))
	{
		LatentAction->EventFinished = true;
	}
	return PlayingID;
}

int32 UAkAudioEvent::PostOnGameObjectAndWait(UAkGameObject* GameObject, const FLatentActionInfo LatentActionInfo)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostOnGameObjectAndWait"));
	if (UNLIKELY(!IsValid(GameObject)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Failed to post latent AkAudioEvent '%s' with a game object that's not valid. The gane object needs to be valid in order to wait for it."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	auto* World = GameObject->GetWorld();
	if (UNLIKELY(!World))
	{
		UE_LOG(LogAkAudio, Log, TEXT("Failed to post latent AkAudioEvent '%s' with a game object '%s' world that's not valid."), *GetName(), *GameObject->GetName());
		return AK_INVALID_PLAYING_ID;
	}

	FLatentActionManager& LatentActionManager = World->GetLatentActionManager();
	FWaitEndOfEventAction* LatentAction = new FWaitEndOfEventAction(LatentActionInfo);
	LatentActionManager.AddNewAction(LatentActionInfo.CallbackTarget, LatentActionInfo.UUID, LatentAction);

	if (UNLIKELY(!World->AllowAudioPlayback()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' with a game object '%s' world '%s' that doesn't allow audio playback."), *GetName(), *GameObject->GetName(), *World->GetName());
		LatentAction->EventFinished = true;
		return AK_INVALID_PLAYING_ID;
	}

	const auto PlayingID = PostOnGameObject(GameObject, nullptr, nullptr, nullptr, (AkCallbackType)0, LatentAction);
	if (UNLIKELY(PlayingID == AK_INVALID_PLAYING_ID))
	{
		LatentAction->EventFinished = true;
	}
	return PlayingID;
}

int32 UAkAudioEvent::PostAtLocation(const FVector Location, const FRotator Orientation, const FOnAkPostEventCallback& Callback,
                                    const int32 CallbackMask, const UObject* WorldContextObject)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostAtLocation"));
	if (UNLIKELY(!IsValid(WorldContextObject)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Failed to post AkAudioEvent '%s' at location with a World Context Object that's not valid."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	const auto* World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull);
	return PostAtLocation(Location, Orientation, World, &Callback, nullptr, nullptr,
		AkCallbackTypeHelpers::GetCallbackMaskFromBlueprintMask(CallbackMask), nullptr);
}

int32 UAkAudioEvent::ExecuteAction(const AkActionOnEventType ActionType, const AActor* Actor, const int32 PlayingID,
	const int32 TransitionDuration, const EAkCurveInterpolation FadeCurve)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::ExecuteAction"));
	const auto* AudioDevice = FAkAudioDevice::Get();
	if (UNLIKELY(!AudioDevice))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to execute an action on AkAudioEvent '%s' without an Audio Device."), *GetName());
		return AK_NotInitialized;
	}

	if (UNLIKELY(!AudioDevice->IsInitialized()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to execute an action on AkAudioEvent '%s' with the Sound Engine uninitialized."), *GetName());
		return AK_NotInitialized;
	}

	auto* SoundEngine = IWwiseSoundEngineAPI::Get();
	if (UNLIKELY(!SoundEngine))
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to execute an action on AkAudioEvent '%s' without a Sound Engine."), *GetName());
		return AK_NotInitialized;
	}

	if (!Actor)
	{
		return SoundEngine->ExecuteActionOnEvent(GetShortID(),
			static_cast<AK::SoundEngine::AkActionOnEventType>(ActionType),
			AK_INVALID_GAME_OBJECT,
			TransitionDuration,
			static_cast<AkCurveInterpolation>(FadeCurve),
			PlayingID
		);
	}

	if (UNLIKELY(Actor->IsActorBeingDestroyed() || !IsValid(Actor)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Failed to execute on AkAudioEvent '%s' with an actor that's not valid."), *GetName());
		return AK_InvalidParameter;
	}

	UAkComponent* Component = AudioDevice->GetAkComponent(Actor->GetRootComponent(), FName(), nullptr, EAttachLocation::KeepRelativeOffset);
	if (UNLIKELY(!Component))
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to execute on AkAudioEvent '%s' with an actor that doesn't have an AkComponent on Root."), *GetName());
		return AK_InvalidParameter;
	}

	return SoundEngine->ExecuteActionOnEvent(GetShortID(),
		static_cast<AK::SoundEngine::AkActionOnEventType>(ActionType),
		Component->GetAkGameObjectID(),
		TransitionDuration,
		static_cast<AkCurveInterpolation>(FadeCurve),
		PlayingID
	);
}

AkPlayingID UAkAudioEvent::PostOnActor(const AActor* Actor, const FOnAkPostEventCallback* Delegate,
	const AkCallbackFunc Callback, void* Cookie, const AkCallbackType CallbackMask, FWaitEndOfEventAction* LatentAction,
	const bool bStopWhenAttachedObjectDestroyed, const EAkAudioContext AudioContext)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostOnActor"));
	const auto* AudioDevice = FAkAudioDevice::Get();
	if (UNLIKELY(!AudioDevice))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' on actor without an Audio Device."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (UNLIKELY(!AudioDevice->IsInitialized()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' with the Sound Engine uninitialized."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (!Actor)
	{
		return PostAmbient(Delegate, Callback, Cookie, CallbackMask, LatentAction, AudioContext);
	}

	if (UNLIKELY(Actor->IsActorBeingDestroyed() || !IsValid(Actor)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Failed to post AkAudioEvent '%s' with an actor that's not valid."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	const auto* World = Actor->GetWorld();
	if (UNLIKELY(!World))
	{
		UE_LOG(LogAkAudio, Log, TEXT("Failed to post AkAudioEvent '%s' with an actor '%s' world that's not valid."), *GetName(), *Actor->GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (UNLIKELY(!World->AllowAudioPlayback()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' with an actor '%s' world '%s' that doesn't allow audio playback."), *GetName(), *Actor->GetName(), *World->GetName());
		return AK_INVALID_PLAYING_ID;
	}

	UAkComponent* Component = AudioDevice->GetAkComponent(Actor->GetRootComponent(), FName(), nullptr, EAttachLocation::KeepRelativeOffset);
	if (UNLIKELY(!Component))
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to post AkAudioEvent '%s' with an actor that doesn't have an AkComponent on Root."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	return PostOnComponent(Component, Delegate, Callback, Cookie, CallbackMask, LatentAction, bStopWhenAttachedObjectDestroyed, AudioContext);
}

AkPlayingID UAkAudioEvent::PostOnComponent(UAkComponent* Component, const FOnAkPostEventCallback* Delegate,
	const AkCallbackFunc Callback, void* Cookie, const AkCallbackType CallbackMask, FWaitEndOfEventAction* LatentAction,
	const bool bStopWhenAttachedObjectDestroyed, const EAkAudioContext AudioContext)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostOnComponent"));
	if (UNLIKELY(!Component))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' with null AkComponent."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (UNLIKELY(!IsValid(Component)))
	{
		UE_LOG(LogAkAudio, Error, TEXT("Failed to post AkAudioEvent '%s' with an AkComponent that's not valid."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (UNLIKELY(Component->StopWhenOwnerDestroyed != bStopWhenAttachedObjectDestroyed))
	{
		UE_LOG(LogWwiseHints, VeryVerbose, TEXT("Updating AkComponent(%s) StopWhenOwnerDestroyed (%s->%s) because of AkAudioEvent '%s'. You should not modify the StopWhenOwnerDestroyed through a PostEvent unless you know what you are doing."),
			*Component->GetName(),
			Component->StopWhenOwnerDestroyed ? TEXT("true") : TEXT("false"),
			bStopWhenAttachedObjectDestroyed ? TEXT("true") : TEXT("false"),
			*GetName());
		Component->StopWhenOwnerDestroyed = bStopWhenAttachedObjectDestroyed;
	}
	return PostOnGameObject(Component, Delegate, Callback, Cookie, CallbackMask, LatentAction, AudioContext);
}

AkPlayingID UAkAudioEvent::PostAtLocation(const FVector& Location, const FRotator& Orientation, const UWorld* World,
	const FOnAkPostEventCallback* Delegate, const AkCallbackFunc Callback, void* Cookie, const AkCallbackType CallbackMask,
	FWaitEndOfEventAction* LatentAction, const EAkAudioContext AudioContext)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostAtLocation"));
	auto* AudioDevice = FAkAudioDevice::Get();
	if (UNLIKELY(!AudioDevice))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' at a location without an Audio Device."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (UNLIKELY(!AudioDevice->IsInitialized()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' at a location with the Sound Engine uninitialized."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	auto* SoundEngine = IWwiseSoundEngineAPI::Get();
	if (UNLIKELY(!SoundEngine))
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to post AkAudioEvent '%s' at a location without a Sound Engine."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (UNLIKELY(!World))
	{
		UE_LOG(LogAkAudio, Log, TEXT("Failed to post AkAudioEvent '%s' at a location without a world world."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (UNLIKELY(!World->AllowAudioPlayback()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' with a world '%s' that doesn't allow audio playback."), *GetName(), *World->GetName());
		return AK_INVALID_PLAYING_ID;
	}

	const AkGameObjectID ObjectID = (AkGameObjectID)this;
	AKRESULT Result = AudioDevice->RegisterGameObject(ObjectID, GetName());
	if (UNLIKELY(Result != AK_Success))
	{
		return AK_INVALID_PLAYING_ID;
	}

	TArray<AkAuxSendValue> AkReverbVolumes;
	AudioDevice->GetAuxSendValuesAtLocation(Location, AkReverbVolumes, World);
	Result = SoundEngine->SetGameObjectAuxSendValues(ObjectID, AkReverbVolumes.GetData(), AkReverbVolumes.Num());
	UE_CLOG(UNLIKELY(Result != AK_Success), LogAkAudio, Log, TEXT("Could not Set AuxSend Values while PostOnLocation for AkAudioEvent '%s' (ObjId: %" PRIu64 "): (%" PRIu32 ") %s."), *GetName(), ObjectID, Result, AkUnrealHelper::GetResultString(Result));

	AkRoomID RoomID = 0;
	auto& RoomIndex = AudioDevice->GetRoomIndex();
	TArray<UAkRoomComponent*> AkRooms = RoomIndex.Query<UAkRoomComponent>(Location, World);
	if (LIKELY(AkRooms.Num() > 0))
	{
		UE_CLOG(AkRooms.Num() > 1, LogAkAudio, Verbose, TEXT("There are %d rooms while PostOnLocation for AkAudioEvent '%s' (ObjId: %" PRIu64 "). Picking the first one."), (int)AkRooms.Num(), *GetName(), ObjectID);
		RoomID = AkRooms[0]->GetRoomID();
		AudioDevice->SetInSpatialAudioRoom(ObjectID, RoomID);
	}
	else
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("No Spatial Audio Room while PostOnLocation AkAudioEvent '%s' (ObjId: %" PRIu64 ")"), *GetName(), ObjectID);
	}

	AkSoundPosition SoundPosition;
	FQuat OrientationQuat(Orientation);
	AudioDevice->FVectorsToAKWorldTransform(Location, OrientationQuat.GetForwardVector(), OrientationQuat.GetUpVector(), SoundPosition);
	Result = SoundEngine->SetPosition(ObjectID, SoundPosition);
	UE_CLOG(UNLIKELY(Result != AK_Success), LogAkAudio, Log, TEXT("Could not Set Position for AkAudioEvent '%s' (ObjId: %" PRIu64 "): (%" PRIu32 ") %s."), *GetName(), ObjectID, Result, AkUnrealHelper::GetResultString(Result));

	const auto PlayingID = PostOnGameObjectID(ObjectID, Delegate, Callback, Cookie, CallbackMask, LatentAction, AudioContext);

	Result = SoundEngine->UnregisterGameObj( ObjectID );
	UE_CLOG(UNLIKELY(Result != AK_Success), LogAkAudio, Log, TEXT("Could not Unregister GameObject after PostOnLocation for AkAudioEvent '%s' (ObjId: %" PRIu64 "): (%" PRIu32 ") %s."), *GetName(), ObjectID, Result, AkUnrealHelper::GetResultString(Result));

	return PlayingID;
}

AkPlayingID UAkAudioEvent::PostAmbient(const FOnAkPostEventCallback* Delegate, AkCallbackFunc Callback, void* Cookie,
	const AkCallbackType CallbackMask, FWaitEndOfEventAction* LatentAction, const EAkAudioContext AudioContext)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostAmbient"));
	return PostOnGameObjectID(DUMMY_GAMEOBJ, Delegate, Callback, Cookie, CallbackMask, LatentAction, AudioContext);
}

AkPlayingID UAkAudioEvent::PostOnGameObject(UAkGameObject* GameObject, const FOnAkPostEventCallback* Delegate,
                                      const AkCallbackFunc Callback, void* Cookie, const AkCallbackType CallbackMask, FWaitEndOfEventAction* LatentAction,
                                      const EAkAudioContext AudioContext)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostOnGameObject"));
	if (UNLIKELY(!GameObject))
	{
		UE_LOG(LogAkAudio, VeryVerbose, TEXT("Failed to post AkAudioEvent without a GameObject."))
		return AK_INVALID_PLAYING_ID;
	}

	UE_CLOG(LIKELY(GameObject->AkAudioEvent) && UNLIKELY(GameObject->AkAudioEvent != this),
		LogWwiseHints, VeryVerbose, TEXT("Posting AkAudioEvent(%s) that is different than the one associated (%s) in the GameObject(%s). You should not have an associated AkAudioEvent if you want to play different Events on the same GameObject."),
		*GetName(),
		*GameObject->AkAudioEvent->GetName(),
		*GameObject->GetName());

	GameObject->UpdateObstructionAndOcclusion();
	const auto GameObjectID = GameObject->GetAkGameObjectID();
	const AkPlayingID PlayingID = PostOnGameObjectID(GameObjectID, Delegate, Callback, Cookie, CallbackMask, LatentAction, AudioContext);
	if (PlayingID != AK_INVALID_PLAYING_ID)
	{
		GameObject->EventPosted();
	}
	return PlayingID;
}

AkPlayingID UAkAudioEvent::PostOnGameObjectID(const AkGameObjectID GameObjectID, const FOnAkPostEventCallback* Delegate,
	const AkCallbackFunc Callback, void* Cookie, const AkCallbackType CallbackMask, FWaitEndOfEventAction* LatentAction,
	const EAkAudioContext AudioContext)
{
	SCOPED_AKAUDIO_EVENT(TEXT("UAkAudioEvent::PostOnGameObjectID"));
	if (Delegate)
	{
		UE_CLOG(UNLIKELY(Callback), LogAkAudio, Error, TEXT("Both Delegate and Callback used for AkAudioEvent('%s').Post(GameObjectID=%" PRIu64 "). Ignoring AkCallback."), *GetName(), GameObjectID);
		UE_CLOG(UNLIKELY(LatentAction), LogAkAudio, Error, TEXT("Both Delegate and LatentAction used for AkAudioEvent('%s').Post(GameObjectID=%" PRIu64 "). Ignoring LatentAction."), *GetName(), GameObjectID);
		UE_CLOG(UNLIKELY(Cookie), LogAkAudio, Error, TEXT("Blueprint Delegate ignores Cookie for AkAudioEvent('%s').Post(GameObjectID=%" PRIu64 ")."), *GetName(), GameObjectID);

		const auto Result = PostEvent(GameObjectID,
			[this, Delegate, CallbackMask](AkGameObjectID GameObjectID)
			{
				auto* AudioDevice = FAkAudioDevice::Get();
				auto* CallbackManager = AudioDevice->GetCallbackManager();
				return CallbackManager->CreateCallbackPackage(*Delegate, CallbackMask, GameObjectID, true);
			},
			AudioContext);
		return Result;
	}
	else if (LatentAction)
	{
		UE_CLOG(UNLIKELY(Callback), LogAkAudio, Error, TEXT("Both LatentAction and Callback used for AkAudioEvent('%s').Post(GameObjectID=%" PRIu64 "). Ignoring AkCallback."), *GetName(), GameObjectID);
		UE_CLOG(UNLIKELY(CallbackMask), LogAkAudio, Error, TEXT("LatentAction ignores CallbackMask for AkAudioEvent('%s').Post(GameObjectID=%" PRIu64 ")."), *GetName(), GameObjectID);
		UE_CLOG(UNLIKELY(Cookie), LogAkAudio, Error, TEXT("LatentAction ignores Cookie for AkAudioEvent('%s').Post(GameObjectID=%" PRIu64 ")."), *GetName(), GameObjectID);

		const auto Result = PostEvent(GameObjectID,
			[this, LatentAction](AkGameObjectID GameObjectID)
			{
				auto* AudioDevice = FAkAudioDevice::Get();
				auto* CallbackManager = AudioDevice->GetCallbackManager();
				return CallbackManager->CreateCallbackPackage(LatentAction, GameObjectID, true);
			},
			AudioContext);
		return Result;
	}
	else
	{
		UE_CLOG(UNLIKELY(!Callback && CallbackMask), LogAkAudio, Error, TEXT("Callback is not set, but there's a CallbackMask for AkAudioEvent('%s').Post(GameObjectID=%" PRIu64 ")."), *GetName(), GameObjectID);
		UE_CLOG(UNLIKELY(!Callback && Cookie), LogAkAudio, Error, TEXT("Callback is not set, but there's a Cookie for AkAudioEvent('%s').Post(GameObjectID=%" PRIu64 ")."), *GetName(), GameObjectID);

		const auto Result = PostEvent(GameObjectID,
			[this, Callback, Cookie, CallbackMask](AkGameObjectID GameObjectID)
			{
				auto* AudioDevice = FAkAudioDevice::Get();
				auto* CallbackManager = AudioDevice->GetCallbackManager();
				return CallbackManager->CreateCallbackPackage(Callback, Cookie, CallbackMask, GameObjectID, true);
			},
			AudioContext);
		return Result;
	}
}

template <typename FCreateCallbackPackage>
AkPlayingID UAkAudioEvent::PostEvent(const AkGameObjectID GameObjectID, FCreateCallbackPackage&& CreateCallbackPackage,
	const EAkAudioContext AudioContext)
{
	SCOPED_AKAUDIO_EVENT_2(TEXT("UAkAudioEvent::PostEvent"));
	auto* AudioDevice = FAkAudioDevice::Get();
	if (UNLIKELY(!AudioDevice))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' without an Audio Device."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (UNLIKELY(!AudioDevice->IsInitialized()))
	{
		UE_LOG(LogAkAudio, Verbose, TEXT("Failed to post AkAudioEvent '%s' with the Sound Engine uninitialized."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	if (!IsLoaded())
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to post AkAudioEvent: Data for '%s' wasn't found. Make sure the GeneratedSoundBanks folder (%s) exists and is properly set in the project settings."), *GetName(), *AkUnrealHelper::GetSoundBankDirectory());
		return AK_INVALID_PLAYING_ID;
	}

	if (!IsDataFullyLoaded())
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to post AkAudioEvent: Not all localization data for '%s' are loaded. Consider using PostEventAsync()."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	auto* CallbackManager = AudioDevice->GetCallbackManager();
	if (UNLIKELY(!CallbackManager))
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to post AkAudioEvent '%s' without a Callback Manager."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	auto* SoundEngine = IWwiseSoundEngineAPI::Get();
	if (UNLIKELY(!SoundEngine))
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to post AkAudioEvent '%s' without a Sound Engine."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	auto* ExternalSourceManager = IWwiseExternalSourceManager::Get();
	if (UNLIKELY(!ExternalSourceManager))
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to post AkAudioEvent '%s' without the External Source Manager."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	auto CallbackPackage = CreateCallbackPackage(GameObjectID);
	if (UNLIKELY(!CallbackPackage))
	{
		UE_LOG(LogAkAudio, Warning, TEXT("Failed to post AkAudioEvent '%s': Could not create CallbackPackage."), *GetName());
		return AK_INVALID_PLAYING_ID;
	}

	TArray<AkExternalSourceInfo> ExternalSources;
	const TArray<uint32> ExternalSourceMedia = ExternalSourceManager->PrepareExternalSourceInfos(ExternalSources, GetAllExternalSources());

	const auto PlayingID = SoundEngine->PostEvent(
		  GetShortID()
		, GameObjectID
		, CallbackPackage->uUserFlags | AK_EndOfEvent
		, &FAkComponentCallbackManager::AkComponentCallback
		, CallbackPackage
		, ExternalSources.Num()
		, ExternalSources.GetData()
	);

	ExternalSourceManager->BindPlayingIdToExternalSources(PlayingID, ExternalSourceMedia);

	if (UNLIKELY(PlayingID == AK_INVALID_PLAYING_ID))
	{
		UE_LOG(LogAkAudio, Log, TEXT("Failed to post AkAudioEvent '%s'."), *GetName());
		CallbackManager->RemoveCallbackPackage(CallbackPackage, GameObjectID);
		return AK_INVALID_PLAYING_ID;
	}

	AudioDevice->AddPlayingID(GetShortID(), PlayingID, AudioContext);

	UE_LOG(LogAkAudio, VeryVerbose, TEXT("Posted AkAudioEvent '%s' as PlayingId %" PRIu32 "."), *GetName(), PlayingID);
	return PlayingID;
}

void UAkAudioEvent::Serialize(FArchive& Ar)
{
	Super::Serialize(Ar);

	if (HasAnyFlags(RF_ClassDefaultObject))
	{
		return;
	}

#if !UE_SERVER
#if WITH_EDITORONLY_DATA 
	if (Ar.IsCooking() && Ar.IsSaving() && !Ar.CookingTarget()->IsServerOnly())
	{
		FWwiseLocalizedEventCookedData CookedDataToArchive;
		if (auto* ResourceCooker = FWwiseResourceCooker::GetForArchive(Ar))
		{
			ResourceCooker->PrepareCookedData(CookedDataToArchive, GetValidatedInfo(EventInfo));
			FillMetadata(ResourceCooker->GetProjectDatabase());
		}
		CookedDataToArchive.Serialize(Ar);
		Ar << MaximumDuration;
		Ar << MinimumDuration;
		Ar << IsInfinite;
		Ar << MaxAttenuationRadius;
	}
#else
	EventCookedData.Serialize(Ar);
	Ar << MaximumDuration;
	Ar << MinimumDuration;
	Ar << IsInfinite;
	Ar << MaxAttenuationRadius;
#endif
#endif
}

#if WITH_EDITORONLY_DATA
void UAkAudioEvent::FillInfo()
{
	auto* ResourceCooker = FWwiseResourceCooker::GetDefault();
	if (UNLIKELY(!ResourceCooker))
	{
		UE_LOG(LogAkAudio, Error, TEXT("UAkAudioEvent::FillInfo: ResourceCooker not initialized"));
		return;
	}

	auto ProjectDatabase = ResourceCooker->GetProjectDatabase();
	if (UNLIKELY(!ProjectDatabase))
	{
		UE_LOG(LogAkAudio, Error, TEXT("UAkAudioEvent::FillInfo: ProjectDatabase not initialized"));
		return;
	}

	const TSet<FWwiseRefEvent> EventRef = FWwiseDataStructureScopeLock(*ProjectDatabase).GetEvent(GetValidatedInfo(EventInfo));
	if (UNLIKELY(EventRef.Num() == 0))
	{
		UE_LOG(LogAkAudio, Log, TEXT("UAkAudioEvent::FillInfo (%s): Cannot fill Asset Info - Event is not loaded"), *GetName());
		return;
	}

	const FWwiseMetadataEvent* EventMetadata = EventRef.Array()[0].GetEvent();
	if (EventMetadata->Name.IsNone() || !EventMetadata->GUID.IsValid() || EventMetadata->Id == AK_INVALID_UNIQUE_ID) 
	{
		UE_LOG(LogAkAudio, Warning, TEXT("UAkAudioEvent::FillInfo: Valid object not found in Project Database"));
		return;
	}
	EventInfo.WwiseGuid = EventMetadata->GUID;
	EventInfo.WwiseShortId = EventMetadata->Id;
	EventInfo.WwiseName = EventMetadata->Name;
}

void UAkAudioEvent::FillMetadata(FWwiseProjectDatabase* ProjectDatabase)
{
	Super::FillMetadata(ProjectDatabase);

	const TSet<FWwiseRefEvent> EventRef = FWwiseDataStructureScopeLock(*ProjectDatabase).GetEvent(GetValidatedInfo(EventInfo));
	if (UNLIKELY(EventRef.Num() == 0))
	{
		UE_LOG(LogAkAudio, Log, TEXT("UAkAudioEvent::FillMetadata (%s): Cannot fill Metadata - Event not found in Project Database"), *GetName());
		return;
	}

	const FWwiseMetadataEvent* EventMetadata = EventRef.Array()[0].GetEvent();
	if (EventMetadata->Name.IsNone() || !EventMetadata->GUID.IsValid() || EventMetadata->Id == AK_INVALID_UNIQUE_ID)
	{
		UE_LOG(LogAkAudio, Warning, TEXT("UAkAudioEvent::FillMetadata: Valid object not found in Project Database"));
		return;
	}

	MaximumDuration = EventMetadata->DurationMax;
	MinimumDuration = EventMetadata->DurationMin;
	IsInfinite = EventMetadata->DurationType == EWwiseMetadataEventDurationType::Infinite;
	MaxAttenuationRadius = EventMetadata->MaxAttenuation;
}

#endif

void UAkAudioEvent::LoadEventData()
{
	SCOPED_AKAUDIO_EVENT_2(TEXT("LoadEventData"));
	auto* ResourceLoader = FWwiseResourceLoader::Get();
	if (UNLIKELY(!ResourceLoader))
	{
		return;
	}

	if (LoadedEvent)
	{
		UnloadEventData(false);
	}
	
#if WITH_EDITORONLY_DATA
	if (IWwiseProjectDatabaseModule::IsInACookingCommandlet())
	{
		return;
	}
	auto* ProjectDatabase = FWwiseProjectDatabase::Get();
	if (!ProjectDatabase || !ProjectDatabase->IsProjectDatabaseParsed())
	{
		UE_LOG(LogAkAudio, VeryVerbose, TEXT("UAkAudioEvent::LoadEventData: Not loading '%s' because project database is not parsed."), *GetName())
		return;
	}
	auto* ResourceCooker = FWwiseResourceCooker::GetDefault();
	if (UNLIKELY(!ResourceCooker))
	{
		return;
	}
	if (UNLIKELY(!ResourceCooker->PrepareCookedData(EventCookedData, GetValidatedInfo(EventInfo))))
	{
		return;
	}
	FillMetadata(ResourceCooker->GetProjectDatabase());
#endif

	UE_LOG(LogAkAudio, Verbose, TEXT("%s - LoadEventData"), *GetName());
	LoadedEvent = ResourceLoader->LoadEvent(EventCookedData);
}

#if WITH_EDITOR
void UAkAudioEvent::LoadEventDataForContentBrowserPreview()
{
	if(!bAutoLoad)
	{
		OnBeginPIEDelegateHandle = FEditorDelegates::BeginPIE.AddUObject(this, &UAkAudioEvent::OnBeginPIE);
	}

	LoadEventData();
}

void UAkAudioEvent::OnBeginPIE(const bool bIsSimulating)
{
	FEditorDelegates::BeginPIE.Remove(OnBeginPIEDelegateHandle);
	OnBeginPIEDelegateHandle.Reset();
	UnloadEventData(false);
}

#endif

void UAkAudioEvent::BeginDestroy()
{
	Super::BeginDestroy();

#if WITH_EDITOR
	if (OnBeginPIEDelegateHandle.IsValid())
	{
		FEditorDelegates::BeginPIE.Remove(OnBeginPIEDelegateHandle);
		OnBeginPIEDelegateHandle.Reset();
	}
#endif
}

void UAkAudioEvent::UnloadEventData(bool bAsync)
{
	if (LoadedEvent)
	{
		auto* ResourceLoader = FWwiseResourceLoader::Get();
		if (UNLIKELY(!ResourceLoader))
		{
			return;
		}
		UE_LOG(LogAkAudio, Verbose, TEXT("%s - UnloadEventData"), *GetName());
		if (bAsync)
		{
			FWwiseLoadedEventPromise Promise;
			Promise.EmplaceValue(MoveTemp(LoadedEvent));
			ResourceUnload = ResourceLoader->UnloadEventAsync(Promise.GetFuture());
		}
		else
		{
			ResourceLoader->UnloadEvent(MoveTemp(LoadedEvent));
		}
		LoadedEvent = nullptr;
	}
}

bool UAkAudioEvent::IsDataFullyLoaded() const
{
	if (!LoadedEvent)
	{
		return false;
	}

	return LoadedEvent->GetValue().LoadedData.IsLoaded();
}

bool UAkAudioEvent::IsLoaded() const
{
	return LoadedEvent != nullptr;
}

#if WITH_EDITORONLY_DATA
bool UAkAudioEvent::ObjectIsInSoundBanks()
{
	auto* ResourceCooker = FWwiseResourceCooker::GetDefault();
	if (UNLIKELY(!ResourceCooker))
	{
		UE_LOG(LogAkAudio, Error, TEXT("UAkAudioEvent::GetWwiseRef: ResourceCooker not initialized"));
		return false;
	}

	auto ProjectDatabase = ResourceCooker->GetProjectDatabase();
	if (UNLIKELY(!ProjectDatabase))
	{
		UE_LOG(LogAkAudio, Error, TEXT("UAkAudioEvent::GetWwiseRef: ProjectDatabase not initialized"));
		return false;
	}

	const TSet<FWwiseRefEvent> EventRef = FWwiseDataStructureScopeLock(*ProjectDatabase).GetEvent(GetValidatedInfo(EventInfo));
	if (UNLIKELY(EventRef.Num() == 0))
	{
		UE_LOG(LogAkAudio, Log, TEXT("UAkAudioEvent::GetWwiseRef (%s): Event is not loaded"), *GetName());
		return false;
	}

	return EventRef.Array()[0].IsValid();
}
#endif

TArray<FWwiseExternalSourceCookedData> UAkAudioEvent::GetAllExternalSources() const
{
	if (!LoadedEvent)
	{
		return {};
	}

	const auto& EventData = LoadedEvent->GetValue();
	if (!EventData.LoadedData.IsLoaded())
	{
		return {};
	}

	const auto& LoadedLanguage = EventData.LanguageRef;
	const FWwiseEventCookedData* CookedData = EventCookedData.EventLanguageMap.Find(LoadedLanguage);
	if (UNLIKELY(!CookedData))
	{
		return {};
	}

	auto Result = CookedData->ExternalSources;
	for (const auto& Leaf : CookedData->SwitchContainerLeaves)
	{
		Result.Append(Leaf.ExternalSources);
	}
	return Result;
}

#if WITH_EDITORONLY_DATA
void UAkAudioEvent::CookAdditionalFilesOverride(const TCHAR* PackageFilename, const ITargetPlatform* TargetPlatform,
	TFunctionRef<void(const TCHAR* Filename, void* Data, int64 Size)> WriteAdditionalFile)
{
	if (HasAnyFlags(RF_ClassDefaultObject))
	{
		return;
	}

	FWwiseResourceCooker* ResourceCooker = FWwiseResourceCooker::GetForPlatform(TargetPlatform);
	if (!ResourceCooker)
	{
		return;
	}
	ResourceCooker->SetSandboxRootPath(PackageFilename);
	ResourceCooker->CookEvent(GetValidatedInfo(EventInfo), WriteAdditionalFile);
}
#endif