/*******************************************************************************
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.
*******************************************************************************/

#include "AkAssetMigrationHelper.h"

#include "AkAssetDatabase.h"
#include "AkAudioEvent.h"
#include "AkAuxBus.h"
#include "AkDeprecated.h"
#include "AkAudioBank.h"
#include "AkWaapiClient.h"
#include "AkWaapiUtils.h"
#include "AkMigrationWidgets.h"
#include "AkCustomVersion.h"
#include "AkUnrealEditorHelper.h"
#include "AkUnrealHelper.h"
#include "WwiseDefines.h"
#include "IAudiokineticTools.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetTools/Public/AssetToolsModule.h"

#include "GenericPlatform/GenericPlatformFile.h"
#if UE_5_0_OR_LATER
#include "HAL/PlatformFileManager.h"
#else
#include "HAL/PlatformFilemanager.h"
#endif
#include "UnrealEd/Public/ObjectTools.h"
#include "UnrealEd/Public/FileHelpers.h"
#include "Misc/FileHelper.h"
#include "IDesktopPlatform.h"
#include "DesktopPlatformModule.h"
#include "Interfaces/IMainFrameModule.h"
#include "Mathematics/APConversion.h"
#include "Misc/App.h"

#define LOCTEXT_NAMESPACE "AkAudio"

namespace AkAssetMigration
{
	void PromptMigration(const FMigrationContext& MigrationOptions, FMigrationOperations& OutMigrationOperations)
	{
		if (!FApp::CanEverRender())
		{
			OutMigrationOperations.bCancelled =true;
			return;
		}

		TSharedPtr<SWindow> Dialog = SNew(SWindow)
			.Title(LOCTEXT("BankMigrationDialog", "Wwise Integration Migration"))
			.SupportsMaximize(false)
			.SupportsMinimize(false)
			.FocusWhenFirstShown(true)
			.HasCloseButton(false)
			.SizingRule(ESizingRule::Autosized);

		TSharedPtr<SMigrationWidget> MigrationWidget;

		Dialog->SetContent(
			SAssignNew(MigrationWidget, SMigrationWidget)
			.Dialog(Dialog)
			.ShowBankTransfer(MigrationOptions.bBanksInProject)
			.ShowDeprecatedAssetCleanup(MigrationOptions.bDeprecatedAssetsInProject)
			.ShowAssetMigration(MigrationOptions.bAssetsNeedMigration)
			.ShowProjectMigration(MigrationOptions.bProjectSettingsNotUpToDate)
			.NumDeprecatedAssets(MigrationOptions.NumDeprecatedAssetsInProject)
		);

		FSlateApplication::Get().AddModalWindow(Dialog.ToSharedRef(), nullptr);
		
		if (MigrationOptions.bBanksInProject)
		{
			OutMigrationOperations.BankTransferMethod = MigrationWidget->BankTransferWidget->BankTransferMethod;
			OutMigrationOperations.bDoBankCleanup = MigrationWidget->BankTransferWidget->DeleteSoundBanksCheckBox->IsChecked();
			OutMigrationOperations.bTransferAutoload = MigrationWidget->BankTransferWidget->TransferAutoLoadCheckBox->IsChecked();
			OutMigrationOperations.DefinitionFilePath = MigrationWidget->BankTransferWidget->SoundBankDefinitionFilePath;
		}
		if (MigrationOptions.bDeprecatedAssetsInProject)
		{
			OutMigrationOperations.bDoDeprecatedAssetCleanup = MigrationWidget->DeprecatedAssetCleanupWidget->DeleteAssetsCheckBox->IsChecked();
		}
		if (MigrationOptions.bAssetsNeedMigration)
		{
			OutMigrationOperations.bDoAssetMigration = MigrationWidget->AssetMigrationWidget->MigrateAssetsCheckBox->IsChecked();
		}
		if (MigrationOptions.bProjectSettingsNotUpToDate)
		{
			OutMigrationOperations.bDoProjectUpdate = MigrationWidget->ProjectMigrationWidget->AutoMigrateCheckbox->IsChecked();
			OutMigrationOperations.GeneratedSoundBankDirectory = MigrationWidget->ProjectMigrationWidget->GeneratedSoundBanksFolderPickerWidget->GetDirectory();
		}
		OutMigrationOperations.bCancelled = MigrationWidget->bCancel;
	}

	bool PromptFailedBankTransfer(const FString& ErrorMessage)
	{
		if (!FApp::CanEverRender())
		{
			return false;
		}
		TSharedPtr<SWindow> Dialog = SNew(SWindow)
			.Title(LOCTEXT("BankTransfer", "Bank Transfer Failure"))
			.SupportsMaximize(false)
			.SupportsMinimize(false)
			.FocusWhenFirstShown(true)
			.HasCloseButton(false)
			.SizingRule(ESizingRule::Autosized);

		TSharedPtr<SBankMigrationFailureWidget> FailedBankTransferWidget;

		Dialog->SetContent(
			SAssignNew(FailedBankTransferWidget, SBankMigrationFailureWidget)
			.Dialog(Dialog)
			.ErrorMessage(FText::Format( LOCTEXT("BankTransferErrorMessage", "{0}"), FText::FromString(ErrorMessage)))
		);

		FSlateApplication::Get().AddModalWindow(Dialog.ToSharedRef(), nullptr);
		return !FailedBankTransferWidget->bCancel;
	}

		
	FString FormatWaapiErrorMessage(const TArray<FBankTransferError>& ErrorMessages)
	{
		FString CombinedErrors;
		for (auto BankErrors : ErrorMessages)
		{
			if (BankErrors.bHasBankEntry)
			{
				CombinedErrors += "SoundBank: " + BankErrors.BankEntry.BankAssetData.AssetName.ToString() + "\n";
			}
			CombinedErrors += "Error: " + BankErrors.ErrorMessage + "\n";
			if (BankErrors.bHasBankEntry)
			{
				CombinedErrors += "Wwise Assets in SoundBank : \n";

				for (auto LinkedAsset : BankErrors.BankEntry.LinkedEvents)
				{
					CombinedErrors += FString::Format(TEXT("GUID: {0} - Name: {1}\n"), { LinkedAsset.WwiseGuid.ToString(), LinkedAsset.AssetName });
				}
				for (auto LinkedAsset : BankErrors.BankEntry.LinkedAuxBusses)
				{
					CombinedErrors += FString::Format(TEXT("GUID: {0} - Name: {1}\n"), { LinkedAsset.WwiseGuid.ToString(), LinkedAsset.AssetName });
				}
			}
		}
		return CombinedErrors;
	}

	void FindDeprecatedAssets(TArray<FAssetData>& OutDeprecatedAssets)
	{
		OutDeprecatedAssets.Empty();
		FARFilter Filter;
#if UE_5_1_OR_LATER
		Filter.ClassPaths.Add(UAkMediaAsset::StaticClass()->GetClassPathName());
		Filter.ClassPaths.Add(UAkLocalizedMediaAsset::StaticClass()->GetClassPathName());
		Filter.ClassPaths.Add(UAkExternalMediaAsset::StaticClass()->GetClassPathName());
		Filter.ClassPaths.Add(UAkFolder::StaticClass()->GetClassPathName());
		Filter.ClassPaths.Add(UAkAssetPlatformData::StaticClass()->GetClassPathName());
#else
		Filter.ClassNames.Add(UAkMediaAsset::StaticClass()->GetFName());
		Filter.ClassNames.Add(UAkLocalizedMediaAsset::StaticClass()->GetFName());
		Filter.ClassNames.Add(UAkExternalMediaAsset::StaticClass()->GetFName());
		Filter.ClassNames.Add(UAkFolder::StaticClass()->GetFName());
		Filter.ClassNames.Add(UAkAssetPlatformData::StaticClass()->GetFName());
#endif
		auto& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
		AssetRegistryModule.Get().GetAssets(Filter, OutDeprecatedAssets);
	}

	bool DeleteAssets(const TArray<FAssetData>& InAssetsToDelete)
	{
		/*
		 *Deleting an Asset that is referenced somewhere needs a confirmation which is impossible using a commandlet.
		 *Calling ForceDelete on the UObjects of the Assets will ignore the confirmation and delete the assets.
		*/
		if(!FApp::CanEverRender())
		{
			TArray<UObject*> Objects;
			for(auto Asset : InAssetsToDelete)
			{
				Objects.Add(Asset.GetAsset());
			}
			int DeletedObjects = ObjectTools::ForceDeleteObjects(Objects, false);
			return DeletedObjects == Objects.Num();
		}
		else
		{
			int DeletedObjects = ObjectTools::DeleteAssets(InAssetsToDelete, true);
			return DeletedObjects == InAssetsToDelete.Num();
		}
	}

	bool DeleteDeprecatedAssets(const TArray<FAssetData>& InAssetsToDelete)
	{
		bool bSuccess = true;
		bSuccess &= DeleteAssets(InAssetsToDelete);
		
		const FString MediaFolderpath = FPaths::Combine(AkUnrealEditorHelper::GetLegacySoundBankDirectory(), AkUnrealHelper::MediaFolderName);
		IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

		PlatformFile.DeleteDirectoryRecursively(*MediaFolderpath);

		const FString LocalizedFolderPath = FPaths::Combine(AkUnrealEditorHelper::GetLegacySoundBankDirectory(), AkUnrealEditorHelper::LocalizedFolderName);
		PlatformFile.DeleteDirectoryRecursively(*LocalizedFolderPath);

		return bSuccess;
	}

	void FindWwiseAssetsInProject(TArray<FAssetData>& OutWwiseAssets)
	{
		FARFilter Filter;
		Filter.bRecursiveClasses = true;
#if UE_5_1_OR_LATER
		Filter.ClassPaths.Add(UAkAudioType::StaticClass()->GetClassPathName());
		//We want to delete these asset types during cleanup so no need to dirty them
		Filter.RecursiveClassPathsExclusionSet.Add(UAkAudioBank::StaticClass()->GetClassPathName());
		Filter.RecursiveClassPathsExclusionSet.Add(UAkFolder::StaticClass()->GetClassPathName());
#else
		Filter.ClassNames.Add(UAkAudioType::StaticClass()->GetFName());
		Filter.RecursiveClassesExclusionSet.Add(UAkAudioBank::StaticClass()->GetFName());
		Filter.RecursiveClassesExclusionSet.Add(UAkFolder::StaticClass()->GetFName());
#endif
		FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
		AssetRegistryModule.Get().GetAssets(Filter, OutWwiseAssets);
	}

	bool MigrateWwiseAssets(const TArray<FAssetData> & WwiseAssets, bool bShouldSplitSwitchContainerMedia)
	{
		TArray<UPackage*> WwisePackagesToSave;
		EWwiseEventSwitchContainerLoading SwitchContainerShouldLoad = bShouldSplitSwitchContainerMedia
			? EWwiseEventSwitchContainerLoading::LoadOnReference
			: EWwiseEventSwitchContainerLoading::AlwaysLoad;

		for (auto& Asset : WwiseAssets)
		{
			UAkAudioType* AssetPtr = Cast<UAkAudioType>(Asset.GetAsset());

			if (AssetPtr)
			{
				AssetPtr->MigrateWwiseObjectInfo();
				if (AssetPtr->GetClass() == UAkAudioEvent::StaticClass())
				{
					UAkAudioEvent* Event = Cast<UAkAudioEvent>(AssetPtr);
					Event->EventInfo.SwitchContainerLoading = SwitchContainerShouldLoad;
				}

				if (!AssetPtr->MarkPackageDirty())
				{					
					UE_LOG(LogAudiokineticTools, Warning, TEXT("Could not dirty asset %s during migration, will still try to save it."), *AssetPtr->GetFullName());
				}
				WwisePackagesToSave.Add(AssetPtr->GetPackage());
			}
			else
			{
				UE_LOG(LogAudiokineticTools, Error, TEXT("Could not get asset '%s' in order to migrate it."), *Asset.AssetName.ToString());
			}
		}

		if (WwisePackagesToSave.Num() > 0)
		{
			if (!UEditorLoadingAndSavingUtils::SavePackages(WwisePackagesToSave, false))
			{
				UE_LOG(LogAudiokineticTools, Warning, TEXT("MigrateWwiseAssets: Could not save assets during asset migration. Please save them manually, or run the migration operation again."));
				return false;
			}
		}

		return true;
	}

	bool MigrateAudioBanks(const FMigrationOperations& MigrationOperations, const bool bCleanupAssets, const bool bWasUsingEBP, const bool bTransferAutoLoad, const FString DefinitionFile)
	{
		TSet<FAssetData> FailedBanks;
		TMap<FString, FBankEntry> BanksToTransfer;
		FillBanksToTransfer(BanksToTransfer);
		const bool bIncludeMedia = !bWasUsingEBP;
		bool bContinueMigration = true;
		bool bTransferSuccess = true;

		FString ErrorMessage;
		if (MigrationOperations.BankTransferMethod == EBankTransferMode::WAAPI)
		{
			TArray<FBankTransferError> ErrorMessages = TransferUserBanksWaapi( BanksToTransfer, FailedBanks, bIncludeMedia);
			ErrorMessage = FormatWaapiErrorMessage(ErrorMessages);
			bTransferSuccess = FailedBanks.Num() == 0;
		}
		else if (MigrationOperations.BankTransferMethod == EBankTransferMode::DefinitionFile)
		{
			auto Result = WriteBankDefinitionFile( BanksToTransfer, bIncludeMedia, DefinitionFile);
			bTransferSuccess = Result == Success;

			if (!bTransferSuccess)
			{
				switch (Result)
				{
					case OpenFailure:
						ErrorMessage = FString::Format(TEXT("Failed to open bank definition file for write '{0}'."), {DefinitionFile});
						break;
					case WriteFailure:
						ErrorMessage = FString::Format(TEXT("Failed to write to bank definition file '{0}'."), {DefinitionFile});
						break;
					default:
						break;
				}

			}
		}

		if (!bTransferSuccess)
		{
			if (!FApp::CanEverRender())
			{
				bContinueMigration = MigrationOperations.bIgnoreBankTransferErrors;
			}
			else //Migrating in Unreal Editor
			{
				bContinueMigration = PromptFailedBankTransfer(ErrorMessage);
			}
		}

		if (!bContinueMigration)
		{
			UE_LOG(LogAudiokineticTools, Error, TEXT("MigrateAudioBanks : Errors encountered while transferring SoundBanks: \n%s"), *ErrorMessage);
			UE_LOG(LogAudiokineticTools, Error, TEXT("MigrateAudioBanks : Migration aborted due to SoundBank transfer errors."));
			return false;
		}

		if (!ErrorMessage.IsEmpty())
		{
			UE_LOG(LogAudiokineticTools, Warning, TEXT("MigrateAudioBanks : Errors encountered while transferring SoundBanks: \n%s"), *ErrorMessage);
			UE_LOG(LogAudiokineticTools, Display, TEXT("MigrateAudioBanks : Migration will continue and ignore SoundBank transfer errors."));
		}

		if (bTransferAutoLoad)
		{
			UE_LOG(LogAudiokineticTools, Display, TEXT("MigrateAudioBanks: Transferring AutoLoad setting from AkAudioBank assets to Event and Aux Bus assets."));

			for (auto& Bank : BanksToTransfer)
			{
				UAkAudioBank* BankAsset = Cast<UAkAudioBank>(Bank.Value.BankAssetData.GetAsset());
				if (!BankAsset)
				{
#if UE_5_1_OR_LATER
					UE_LOG(LogAudiokineticTools, Warning, TEXT("MigrateAudioBanks: Could not load UAkAudioBank Asset '%s'. AutoLoad property will not be transferred. "),
						*Bank.Value.BankAssetData.GetObjectPathString());
#else
					UE_LOG(LogAudiokineticTools, Warning, TEXT("MigrateAudioBanks: Could not load UAkAudioBank Asset '%s'. AutoLoad property will not be transferred. "),
						*Bank.Value.BankAssetData.ObjectPath.ToString());
#endif
					continue;
				}
				if (!BankAsset->AutoLoad_DEPRECATED)
				{
					TArray<FLinkedAssetEntry>& LinkedAuxBusses = Bank.Value.LinkedAuxBusses;
					TArray<FLinkedAssetEntry>& LinkedEvents = Bank.Value.LinkedEvents;
					for (auto& LinkedAuxBus : LinkedAuxBusses)
					{
						bool MigrateSuccess = false;
						auto FoundAssetData = AkAssetDatabase::Get().FindAssetByObjectPath(LinkedAuxBus.AssetPath);
						if (FoundAssetData.IsValid())
						{
							if (auto* BusAsset = Cast<UAkAuxBus>(FoundAssetData.GetAsset()))
							{
								BusAsset->bAutoLoad = false;
								MigrateSuccess = BusAsset->MarkPackageDirty();
							}
						}

						if(!MigrateSuccess)
						{
							UE_LOG(LogAudiokineticTools, Warning, TEXT("MigrateAudioBanks: Could not successfully disable AutoLoad on AuxBus asset %s during migration. It will be automatically loaded."), *LinkedAuxBus.AssetPath);
						}
					}
					for (auto& LinkedEvent : LinkedEvents)
					{
						bool MigrateSuccess = false;
						auto FoundAssetData = AkAssetDatabase::Get().FindAssetByObjectPath(LinkedEvent.AssetPath);
						if (FoundAssetData.IsValid())
						{
							if (auto* EventAsset = Cast<UAkAudioEvent>(FoundAssetData.GetAsset()))
							{
								EventAsset->bAutoLoad = false;
								MigrateSuccess = EventAsset->MarkPackageDirty();
							}
						}

						if (!MigrateSuccess)
						{
							UE_LOG(LogAudiokineticTools, Warning, TEXT("MigrateAudioBanks: Could not successfully disable AutoLoad on Event asset %s during migration. It will be automatically loaded."), *LinkedEvent.AssetPath);
						}
					}
				}
			}
		}
		if (bCleanupAssets)
		{
			UE_LOG(LogAudiokineticTools, Display, TEXT("MigrateAudioBanks: Deleting deprecated AkAudioBank assets."));
			TSet<FAssetData> ProjectBanks;
			for (auto Bank : BanksToTransfer)
			{
				ProjectBanks.Add(Bank.Value.BankAssetData);
			}
			TArray<FAssetData> BanksToDelete = ProjectBanks.Array();
			DeleteAssets(BanksToDelete);
		}
		return true;
	}

	void FillBanksToTransfer(TMap<FString, FBankEntry>& BanksToTransfer)
	{
		auto& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
		auto& AssetRegistry = AssetRegistryModule.Get();

		TArray<FAssetData> Banks;
#if UE_5_1_OR_LATER
		AssetRegistry.GetAssetsByClass(UAkAudioBank::StaticClass()->GetClassPathName(), Banks);
#else
		AssetRegistry.GetAssetsByClass(UAkAudioBank::StaticClass()->GetFName(), Banks);
#endif
		for (FAssetData& BankData : Banks)
		{
			FString BankName = BankData.AssetName.ToString();
			BanksToTransfer.Add(BankName, { BankData, {}, {} });
		}

		TArray< FAssetData> Events;
#if UE_5_1_OR_LATER
		AssetRegistry.GetAssetsByClass(UAkAudioEvent::StaticClass()->GetClassPathName(), Events);
#else
		AssetRegistry.GetAssetsByClass(UAkAudioEvent::StaticClass()->GetFName(), Events);
#endif
		for (FAssetData EventData : Events)
		{
			if (UAkAudioEvent* Event = Cast<UAkAudioEvent>(EventData.GetAsset()))
			{
				if (Event->RequiredBank_DEPRECATED != nullptr)
				{
					FString BankName = Event->RequiredBank_DEPRECATED->GetName();
					FLinkedAssetEntry EventEntry = { Event->GetName(), Event->EventInfo.WwiseGuid, Event->GetPathName() };

					if (BanksToTransfer.Contains(BankName))
					{
						BanksToTransfer[BankName].LinkedEvents.Add(EventEntry);
					}
					else
					{
						FAssetData BankAssetData = FAssetData(Event->RequiredBank_DEPRECATED);
						BanksToTransfer.Add(BankName, { BankAssetData, {EventEntry}, {} });
					}
				}
			}
		}

		TArray< FAssetData> AuxBusses;
#if UE_5_1_OR_LATER
		AssetRegistry.GetAssetsByClass(UAkAuxBus::StaticClass()->GetClassPathName(), AuxBusses);
#else
		AssetRegistry.GetAssetsByClass(UAkAuxBus::StaticClass()->GetFName(), AuxBusses);
#endif
		for (FAssetData AuxBusData : AuxBusses)
		{
			if (UAkAuxBus* AuxBus = Cast<UAkAuxBus>(AuxBusData.GetAsset()))
			{
				if (AuxBus->RequiredBank_DEPRECATED != nullptr)
				{
					FString BankName = AuxBus->RequiredBank_DEPRECATED->GetName();
					FLinkedAssetEntry AuxBusEntry = { AuxBus->GetName(), AuxBus->AuxBusInfo.WwiseGuid, AuxBus->GetPathName() };
					if (BanksToTransfer.Contains(BankName))
					{
						BanksToTransfer[BankName].LinkedAuxBusses.Add(AuxBusEntry);
					}
					else
					{
						FAssetData BankAssetData = FAssetData(AuxBus->RequiredBank_DEPRECATED);
						BanksToTransfer.Add(BankName, { BankAssetData, {}, {AuxBusEntry} });
					}
				}
			}
		}
		UE_LOG(LogAudiokineticTools, Display, TEXT("MigrateAudioBanks: Found %d SoundBank assets in project."), BanksToTransfer.Num());
	}

	TArray<FBankTransferError> TransferUserBanksWaapi(const TMap<FString, FBankEntry>& InBanksToTransfer, TSet<FAssetData>& OutFailedBanks, const bool bIncludeMedia)
	{
		UE_LOG(LogAudiokineticTools, Display, TEXT("MigrateAudioBanks: Transfering SoundBanks with WAAPI."));
		TArray<FBankTransferError> ErrorMessages;
		for (TPair<FString, FBankEntry > BankEntry : InBanksToTransfer)
		{
			FGuid BankID;
			if (!CreateBankWaapi(BankEntry.Key, BankEntry.Value, BankID, ErrorMessages))
			{
				OutFailedBanks.Add(BankEntry.Value.BankAssetData);
				continue;
			}
			if (!SetBankIncludesWaapi(BankEntry.Value, BankID, bIncludeMedia, ErrorMessages))
			{
				OutFailedBanks.Add(BankEntry.Value.BankAssetData);
			}
		}
		SaveProjectWaapi(ErrorMessages);
		return ErrorMessages;
	}

	EDefinitionFileCreationResult WriteBankDefinitionFile(const TMap<FString, FBankEntry>& InBanksToTransfer, const bool bIncludeMedia, const FString DefinitionFilePath)
	{
		UE_LOG(LogAudiokineticTools, Display, TEXT("MigrateAudioBanks: Writing SoundBanks Definition File to '%s'."), *DefinitionFilePath);
		// open file to start writing
		IPlatformFile* PlatformFile = &FPlatformFileManager::Get().GetPlatformFile();;
		FString FileLocation =  IFileManager::Get().ConvertToRelativePath(*DefinitionFilePath);
		TUniquePtr<IFileHandle> FileWriter = TUniquePtr<IFileHandle>(PlatformFile->OpenWrite(*FileLocation));
		if (!FileWriter.IsValid())
		{
			return OpenFailure;
		}

		bool bWriteSuccess = true;
		for (TPair<FString, FBankEntry > BankEntry : InBanksToTransfer)
		{
			bWriteSuccess &= WriteBankDefinition(BankEntry.Value, FileWriter, bIncludeMedia);
		}

		if (!bWriteSuccess)
		{
			return WriteFailure;
		}

		FileWriter->Flush();
		return Success;
	}

	bool WriteBankDefinition(const FBankEntry& BankEntry, TUniquePtr<IFileHandle>& FileWriter, const bool bIncludeMedia)
	{
		bool bWriteSuccess = true;
		FString MediaString = bIncludeMedia? "\tMedia": "";
		for (FLinkedAssetEntry Event : BankEntry.LinkedEvents)
		{
			auto Line = BankEntry.BankAssetData.AssetName.ToString() + TEXT("\t\"") + Event.AssetName + TEXT("\"") + TEXT("\tEvent") + MediaString + TEXT("\tStructure") + LINE_TERMINATOR;
			FTCHARToUTF8 Utf8Formatted(*Line);
			bWriteSuccess &= FileWriter->Write(reinterpret_cast<const uint8*>(Utf8Formatted.Get()), Utf8Formatted.Length());
		}
		for (FLinkedAssetEntry Bus : BankEntry.LinkedAuxBusses)
		{
			auto Line = BankEntry.BankAssetData.AssetName.ToString() + TEXT("\t-AuxBus\t\"") + Bus.AssetName + TEXT("\"") + MediaString + TEXT("\tStructure") + LINE_TERMINATOR;
			FTCHARToUTF8 Utf8Formatted(*Line);
			bWriteSuccess &= FileWriter->Write(reinterpret_cast<const uint8*>(Utf8Formatted.Get()), Utf8Formatted.Length());
		}
		return bWriteSuccess;
	}

	bool CreateBankWaapi(const FString& BankName, const FBankEntry& BankEntry, FGuid& OutBankGuid, TArray<FBankTransferError>& ErrorMessages)
	{
#if AK_SUPPORT_WAAPI
		FAkWaapiClient* WaapiClient = FAkWaapiClient::Get();
		if (!WaapiClient)
		{
			UE_LOG(LogAudiokineticTools, Warning, TEXT("Failed to create SoundBank for <%s>. \nCould not get WAAPI Client."), *BankEntry.BankAssetData.PackageName.ToString());
			ErrorMessages.Add({"Could not get WAAPI Client", true, BankEntry});
			return false;
		}
		else if (!WaapiClient->IsConnected())
		{
			UE_LOG(LogAudiokineticTools, Warning, TEXT("Failed to create SoundBank for <%s>. \nWAAPI Client not connected."), *BankEntry.BankAssetData.PackageName.ToString());
			ErrorMessages.Add({"WAAPI Client not connected", true, BankEntry});
			return false;
		}

		TSharedRef<FJsonObject> Args = MakeShared<FJsonObject>();
		Args->SetStringField(WwiseWaapiHelper::PARENT, TEXT("\\SoundBanks\\Default Work Unit"));
		Args->SetStringField(WwiseWaapiHelper::ON_NAME_CONFLICT, WwiseWaapiHelper::RENAME);
		Args->SetStringField(WwiseWaapiHelper::TYPE,  WwiseWaapiHelper::SOUNDBANK_TYPE);
		Args->SetStringField(WwiseWaapiHelper::NAME, BankName);

		TSharedRef<FJsonObject> Options = MakeShared<FJsonObject>();
		TSharedPtr<FJsonObject> Result;
		FString IdString;
		if (!WaapiClient->Call(ak::wwise::core::object::create, Args, Options, Result))
		{
			FString ErrorMessage;
			if (Result.IsValid())
			{
				Result->TryGetStringField(TEXT("message"), ErrorMessage);
			}
			UE_LOG(LogAudiokineticTools, Warning, TEXT("Failed to create SoundBank for <%s>.\nMessage : <%s>."), *BankEntry.BankAssetData.PackageName.ToString(), *ErrorMessage);
			ErrorMessages.Add({ErrorMessage, true, BankEntry});
			return false;
		}

		if (Result.IsValid() && !Result->TryGetStringField(WwiseWaapiHelper::ID, IdString))
		{
			FString ErrorMessage;
			if (Result.IsValid())
			{
				Result->TryGetStringField(TEXT("message"), ErrorMessage);
			}			
			UE_LOG(LogAudiokineticTools, Warning, TEXT("Failed to create SoundBank for <%s>.\nMessage : <%s>."), *BankEntry.BankAssetData.PackageName.ToString(), *ErrorMessage);
			ErrorMessages.Add({ErrorMessage, true, BankEntry});
			return false; // error parsing Json
		}
		FGuid::ParseExact(IdString, EGuidFormats::DigitsWithHyphensInBraces, OutBankGuid);
		return true;
#else
		UE_LOG(LogAudiokineticTools, Error, TEXT("SetBankIncludesWaapi: WAAPI not supported"));
		ErrorMessages.Add({TEXT("WAAPI not supported"), false, {}});
		return false;
#endif
	}

	bool SetBankIncludesWaapi(const FBankEntry& BankEntry, const FGuid& BankId, const bool bIncludeMedia, TArray<FBankTransferError>& ErrorMessages)
	{
#if AK_SUPPORT_WAAPI
		FAkWaapiClient* WaapiClient = FAkWaapiClient::Get();
		if (!WaapiClient)
		{
			UE_LOG(LogAudiokineticTools, Warning, TEXT("WAAPI command to add Wwise objects to SoundBank '%s' failed. \nCould not get WAAPI Client."), *BankEntry.BankAssetData.PackageName.ToString());
			ErrorMessages.Add({"Could not get WAAPI Client", true, BankEntry});
			return false;
		}
		else if (!WaapiClient->IsConnected())
		{
			UE_LOG(LogAudiokineticTools, Warning, TEXT("WAAPI command to add Wwise objects to SoundBank '%s' failed. \nWAAPI Client not connected."), *BankEntry.BankAssetData.PackageName.ToString());
			ErrorMessages.Add({"WAAPI Client not connected", true, BankEntry});
			return false;
		}

		TSet<FString> IncludeIds;
		for (FLinkedAssetEntry Event : BankEntry.LinkedEvents)
		{
			if (Event.WwiseGuid.IsValid())
			{
				IncludeIds.Add(Event.WwiseGuid.ToString(EGuidFormats::DigitsWithHyphensInBraces));
			}
			else
			{
				IncludeIds.Add(TEXT("Event:") + Event.AssetName);
			}
		}

		for (FLinkedAssetEntry AuxBus : BankEntry.LinkedAuxBusses)
		{
			if (AuxBus.WwiseGuid.IsValid())
			{
				IncludeIds.Add(AuxBus.WwiseGuid.ToString(EGuidFormats::DigitsWithHyphensInBraces));
			}
			else
			{
				IncludeIds.Add(TEXT("AuxBus:") + AuxBus.AssetName);
			}
		}
		if (IncludeIds.Num() < 0)
		{
			return true;
		}

		TArray<TSharedPtr<FJsonValue>> Filters;
		Filters.Add(MakeShared< FJsonValueString>(TEXT("events")));
		Filters.Add(MakeShared< FJsonValueString>(TEXT("structures")));
		if (bIncludeMedia)
		{
			Filters.Add(MakeShared< FJsonValueString>(TEXT("media")));
		}

		TArray<TSharedPtr<FJsonValue>> IncludeIdJson;
		for (const FString IncludedId : IncludeIds)
		{
			TSharedPtr<FJsonObject> IncludedObject = MakeShared< FJsonObject>();
			IncludedObject->SetStringField(WwiseWaapiHelper::OBJECT, IncludedId);
			IncludedObject->SetArrayField(WwiseWaapiHelper::FILTER, Filters);
			IncludeIdJson.Add(MakeShared< FJsonValueObject>(IncludedObject));
		}

		TSharedRef<FJsonObject> Args = MakeShared<FJsonObject>();
		Args->SetStringField(WwiseWaapiHelper::SOUNDBANK_FIELD, BankId.ToString(EGuidFormats::DigitsWithHyphensInBraces));
		Args->SetStringField(WwiseWaapiHelper::OPERATION, TEXT("add"));
		Args->SetArrayField(WwiseWaapiHelper::INCLUSIONS, IncludeIdJson);

		TSharedRef<FJsonObject> Options = MakeShared<FJsonObject>();
		TSharedPtr<FJsonObject> Result;
		if (!WaapiClient->Call(ak::wwise::core::soundbank::setInclusions, Args, Options, Result))
		{
			FString ErrorMessage;
			if (Result.IsValid())
			{
				Result->TryGetStringField(TEXT("message"), ErrorMessage);
			}
			UE_LOG(LogAudiokineticTools, Warning, TEXT("WAAPI command to add Wwise objects to SoundBank '%s' failed. \nError Message : <%s>."), *BankEntry.BankAssetData.PackageName.ToString(), *ErrorMessage);
			ErrorMessages.Add({ErrorMessage, true, BankEntry});
			return false;
		}
		return true;

#else
		UE_LOG(LogAudiokineticTools, Error, TEXT("SetBankIncludesWaapi: WAAPI not supported"));
		ErrorMessages.Add({TEXT("WAAPI not supported"), false, {}});
		return false;
#endif
	}
	
	bool SaveProjectWaapi(TArray<FBankTransferError>& ErrorMessages)
	{
#if AK_SUPPORT_WAAPI
		FAkWaapiClient* WaapiClient = FAkWaapiClient::Get();
		if (!WaapiClient)
		{
			UE_LOG(LogAudiokineticTools, Warning, TEXT("Failed to save Wwise project.\n Could not get Waapi Client."));
			ErrorMessages.Add({TEXT("Failed to save Wwise project over WAAPI. Please save the project manually."), false, {}});
			return false;
		}
		else if (!WaapiClient->IsConnected())
		{
			UE_LOG(LogAudiokineticTools, Warning, TEXT("Failed to save Wwise project.\n Waapi Client not connected."));
			ErrorMessages.Add({TEXT("Failed to save Wwise project over WAAPI. Please save the project manually."), false, {}});
			return false;
		}

		TSharedRef<FJsonObject> Args = MakeShared<FJsonObject>(); 
		TSharedRef<FJsonObject> Options = MakeShared<FJsonObject>();
		TSharedPtr<FJsonObject> Result;
		FString IdString;
		if (!WaapiClient->Call(ak::wwise::core::project::save, Args, Options, Result))
		{
			FString ErrorMessage;
			if (Result.IsValid())
			{
				Result->TryGetStringField(TEXT("message"), ErrorMessage);
			}
			UE_LOG(LogAudiokineticTools, Warning, TEXT("Failed to save Wwise project.\nMessage : <%s>."), *ErrorMessage);
			ErrorMessages.Add({TEXT("Failed to save Wwise project over WAAPI. Please save the project manually."), false, {}});
			ErrorMessages.Add({ErrorMessage, false, {}});
			return false;
		}
		return true;
#else
		UE_LOG(LogAudiokineticTools, Error, TEXT("SetBankIncludesWaapi: WAAPI not supported"));
		ErrorMessages.Add({TEXT("WAAPI not supported"), false, {}});
		return false;
#endif
	}

	bool MigrateProjectSettings(FString& ProjectContent, const bool bWasUsingEBP, const bool bUseGeneratedSubFolders, const FString& GeneratedSoundBanksFolder )
	{
		//migrate split media per id
		TArray<PropertyToChange> PropertiesToAdd;
		if (bWasUsingEBP)
		{
			PropertiesToAdd.Add({ TEXT("AutoSoundBankEnabled"), TEXT("True"), TEXT("<Property Name=\"AutoSoundBankEnabled\" Type=\"bool\" Value=\"True\"/>") });
		}

		if (bUseGeneratedSubFolders)
		{
			PropertiesToAdd.Add({ TEXT("MediaAutoBankSubFolders"), TEXT("True"), TEXT("<Property Name=\"MediaAutoBankSubFolders\" Type=\"bool\" Value=\"True\"/>") });
		}

		static const TArray<FString> LogCentralItemsToRemove = 
		{
			TEXT("<IgnoreItem MessageId=\"MediaDuplicated\"/>"),
			TEXT("<IgnoreItem MessageId=\"MediaNotFound\"/>")
		};

		bool bModified = false;
		if (PropertiesToAdd.Num() >0)
		{
			bModified = InsertProperties(PropertiesToAdd, ProjectContent);
		}
		for (const FString& LogItemToRemove : LogCentralItemsToRemove)
		{
			if (ProjectContent.Contains(LogItemToRemove))
			{
				ProjectContent.ReplaceInline(*LogItemToRemove, TEXT(""));
				bModified = true;
			}
		}
		return bModified;
	}
		
	bool SetStandardSettings(FString& ProjectContent)
	{
		static const TArray<PropertyToChange> PropertiesToAdd = {
			{ TEXT("GenerateMultipleBanks"), TEXT("True"), TEXT("<Property Name=\"GenerateMultipleBanks\" Type=\"bool\" Value=\"True\"/>") },
			{ TEXT("GenerateSoundBankJSON"), TEXT("True"), TEXT("<Property Name=\"GenerateSoundBankJSON\" Type=\"bool\" Value=\"True\"/>") },
			{ TEXT("SoundBankGenerateEstimatedDuration"), TEXT("True"), TEXT("<Property Name=\"SoundBankGenerateEstimatedDuration\" Type=\"bool\" Value=\"True\"/>") },
			{ TEXT("SoundBankGenerateMaxAttenuationInfo"), TEXT("True"), TEXT("<Property Name=\"SoundBankGenerateMaxAttenuationInfo\" Type=\"bool\" Value=\"True\"/>") },
			{ TEXT("SoundBankGeneratePrintGUID"), TEXT("True"), TEXT("<Property Name=\"SoundBankGeneratePrintGUID\" Type=\"bool\" Value=\"True\"/>") },
			{ TEXT("SoundBankGeneratePrintPath"), TEXT("True"), TEXT("<Property Name=\"SoundBankGeneratePrintPath\" Type=\"bool\" Value=\"True\"/>") },
			{ TEXT("CopyLooseStreamedMedia"), TEXT("True"), TEXT("<Property Name=\"CopyLooseStreamedMedia\" Type=\"bool\" Value=\"True\"/>") },
			{ TEXT("RemoveUnusedGeneratedFiles"), TEXT("True"), TEXT("<Property Name=\"RemoveUnusedGeneratedFiles\" Type=\"bool\" Value=\"True\"/>") },
		};

		return InsertProperties(PropertiesToAdd, ProjectContent);
	}

	bool InsertProperties(const TArray<PropertyToChange>& PropertiesToChange, FString& ProjectContent)
	{
		static const auto PropertyListStart = TEXT("<PropertyList>");
		static const FString EndTag = TEXT(">");
		static const TCHAR EmptyElementEndChar = '/';
		static const FString ValueTag = TEXT("<Value>");
		static const FString EndValueTag = TEXT("</Value>");

		static const FString ValueAttribute = TEXT("Value=\"");
		static const FString EndValueAttribute = TEXT("\"");

		bool bModified = false;

		int32 PropertyListPosition = ProjectContent.Find(PropertyListStart);
		if (PropertyListPosition != -1)
		{
			int32 InsertPosition = PropertyListPosition + FCString::Strlen(PropertyListStart);

			for (PropertyToChange ItemToAdd : PropertiesToChange)
			{
				auto idx = ProjectContent.Find(ItemToAdd.Name);
				if (idx == -1)
				{
					ProjectContent.InsertAt(InsertPosition, FString::Printf(TEXT("\n\t\t\t\t%s"), *ItemToAdd.Xml));
					bModified = true;
				}
				else
				{
					FString ValueText;
					FString EndValueText;
					int32 EndTagIdx = ProjectContent.Find(EndTag, ESearchCase::IgnoreCase, ESearchDir::FromStart, idx);
					if (ProjectContent[EndTagIdx - 1] == EmptyElementEndChar)
					{
						// The property is an empty element, the value will be in an attribute
						ValueText = ValueAttribute;
						EndValueText = EndValueAttribute;
					}
					else
					{
						// We are in a ValueList
						ValueText = ValueTag;
						EndValueText = EndValueTag;
					}

					int32 ValueIdx = ProjectContent.Find(ValueText, ESearchCase::IgnoreCase, ESearchDir::FromStart, idx);
					int32 EndValueIdx = ProjectContent.Find(EndValueText, ESearchCase::IgnoreCase, ESearchDir::FromStart, ValueIdx);
					if (ValueIdx != -1 && ValueIdx > idx && ValueIdx < EndValueIdx)
					{
						ValueIdx += ValueText.Len();
						auto ValueEndIdx = ProjectContent.Find(EndValueText, ESearchCase::IgnoreCase, ESearchDir::FromStart, ValueIdx);
						if (ValueEndIdx != -1)
						{
							FString value = ProjectContent.Mid(ValueIdx, ValueEndIdx - ValueIdx);
							if (value != ItemToAdd.Value)
							{
								ProjectContent.RemoveAt(ValueIdx, ValueEndIdx - ValueIdx, false);
								ProjectContent.InsertAt(ValueIdx, ItemToAdd.Value);
								bModified = true;
							}
						}
					}
					else
					{
						UE_LOG(LogAudiokineticTools, Warning, TEXT("Could not change value for %s in Wwise project. Some features might not work properly."), *ItemToAdd.Name);
					}
				}
			}
		}

		return bModified;
	}
}

#undef LOCTEXT_NAMESPACE