#include "PortaAssetSubsystem.h"
#include "PortaAvalancheSubsystem.h"
#include "PortaConnectionSubsystem.h"
#include "PortaInterface.h"
#include "PortaSettings.h"
#include "PortaUtils.h"

#include "FileMediaSource.h"
#include "IImageWrapper.h"
#include "IImageWrapperModule.h"
#include "ImageUtils.h"
#include "IRemoteControlModule.h"
#include "LevelSequence.h"
#include "MediaTexture.h"
#if ENGINE_MINOR_VERSION > 0
#include "RCVirtualProperty.h"
#endif
#include "RemoteControlBinding.h"
#include "RemoteControlPreset.h"
#include "TextureResource.h"
#include "TimerManager.h"

#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Components/StaticMeshComponent.h"
#include "HAL/PlatformFileManager.h"
#include "Kismet/GameplayStatics.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Misc/Base64.h"
#include "Misc/FileHelper.h"
#include "Misc/ObjectThumbnail.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "PaperFlipbookComponent.h"
#include "Runtime/Launch/Resources/Version.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonTypes.h"

#if WITH_EDITOR
#include "Editor.h"
#include "ObjectTools.h"
#endif

EPortaMediaStatus UPageInfo::GetStatus()
{
	bool Ready = true;
	for (auto Item : Media)
	{
		switch (Item->Status)
		{
		case EPortaMediaStatus::Error:
			return EPortaMediaStatus::Error;
			break;
		case EPortaMediaStatus::Downloading:
			Ready = false;
			break;
		}
	}

	return Ready ? EPortaMediaStatus::Ready : EPortaMediaStatus::Downloading;
}

void UPageInfo::OnMediaReady(EPortaMediaStatus)
{
	BroadcastStatus();
}

void UPageInfo::BroadcastStatus()
{
	auto NewStatus = GetStatus();
	if (NewStatus != Status)
	{
		Status = NewStatus;
		OnStatusChanged.Broadcast(Status);
	}
}

void UPortaAssetSyncRetryer::Tick(float DeltaTime)
{
	if (TimeUntilRetry <= 0)
		return;

	if ((TimeUntilRetry -= DeltaTime) < 0)
	{
		UPortaAssetSubsystem::DoFullAssetSyncStatic();
	}
}

bool UPortaAssetSyncRetryer::IsTickable() const
{
	return !IsDefaultSubobject() && IsValid(this);
}

TStatId UPortaAssetSyncRetryer::GetStatId() const
{
	RETURN_QUICK_DECLARE_CYCLE_STAT(UPortaHeartbeat, STATGROUP_Tickables);
}


UPortaAssetSubsystem::UPortaAssetSubsystem() :
	AvailableRequests(10)
{

}

void UPortaAssetSubsystem::Start()
{
	AudioLibrary = URuntimeAudioImporterLibrary::CreateRuntimeAudioImporter();
	AudioLibrary->OnResultNative.AddUObject(this, &UPortaAssetSubsystem::OnAudioImportResult);

	auto Config = GetMutableDefault<UPortaSettings>();
	AssetCacheFolder = Config->AssetCacheFolder;
	if (!AssetCacheFolder.EndsWith(TEXT("/")))
		AssetCacheFolder.Append(TEXT("/"));

	PortaApiUrl = Config->PortaApiUrl;
	if (!PortaApiUrl.IsEmpty() && !PortaApiUrl.EndsWith(TEXT("/")))
		PortaApiUrl.Append(TEXT("/"));
	PortaApiKey = Config->PortaApiKey;
	PortaChannel = Config->PortaChannel;

	PresetLibrary = UObjectLibrary::CreateLibrary(URemoteControlPreset::StaticClass(), false, GIsEditor);
	PresetLibrary->bRecursivePaths = true;
	PresetLibrary->LoadAssetDataFromPath(TEXT("/Game"));

	SequenceLibrary = UObjectLibrary::CreateLibrary(ULevelSequence::StaticClass(), false, GIsEditor);
	SequenceLibrary->bRecursivePaths = true;
	SequenceLibrary->LoadAssetDataFromPath(TEXT("/Game"));
	DoFullAssetSync();
}

void UPortaAssetSubsystem::Stop()
{
	AudioLibrary->OnResultNative.RemoveAll(this);
	AudioLibrary = nullptr;
	PresetLibrary = nullptr;
}

void UPortaAssetSubsystem::OnGameInstanceStart()
{
	PresetLibrary->LoadAssetDataFromPath(TEXT("/Game"));
}

void UPortaAssetSubsystem::DoFullAssetSyncStatic()
{
	PortaUtils::CallEngineSubsystemMember(&UPortaAssetSubsystem::DoFullAssetSync);
}

void UPortaAssetSubsystem::DoFullAssetSync()
{
	if (PortaApiUrl.IsEmpty() || PortaApiKey.IsEmpty())
		return;

	Retryer = nullptr;

	auto Request = FHttpModule::Get().CreateRequest();
	Request->OnProcessRequestComplete().BindUObject(this, &UPortaAssetSubsystem::HandleFullAssetSyncResponse);
	Request->SetURL(PortaApiUrl + TEXT("api/playlists?has_media=1&api_key=") + PortaApiKey);
	Request->SetVerb(TEXT("GET"));
	Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
	Request->SetHeader("Content-Type", TEXT("application/json"));
	Request->ProcessRequest();
}

void UPortaAssetSubsystem::QueryPlaylistPages(int Id)
{
	auto Request = FHttpModule::Get().CreateRequest();
	Request->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful) {
		if (!bWasSuccessful || !Response)
			return;

		TArray<TSharedPtr<FJsonValue>> ResponseObject;
		if (Response == nullptr)
			return;
		auto Reader = TJsonReaderFactory<TCHAR>::Create(Response->GetContentAsString());
		if (!FJsonSerializer::Deserialize(Reader, ResponseObject))
			return;

		for (auto Row : ResponseObject)
		{
			const TSharedPtr<FJsonObject>* Page;
			if (Row->TryGetObject(Page))
			{
				int PageId;
				if (!(*Page)->TryGetNumberField(TEXT("id"), PageId))
					continue;

				const TArray<TSharedPtr<FJsonValue>>* Media;
				if ((*Page)->TryGetArrayField(TEXT("media"), Media))
				{
					TSet<FString> Urls;
					for (auto iter : *Media)
					{
						const TSharedPtr<FJsonObject>* Entry;
						if (iter->TryGetObject(Entry))
						{
							FString Url;
							if ((*Entry)->TryGetStringField(TEXT("url"), Url))
							{
								Urls.Add(Url);
							}
						}
					}

					if (Urls.Num() > 0)
					{
						auto PageData = Pages.FindRef(PageId);
						if (PageData)
						{
							PageData->Media.Empty();
						}
						else
						{
							PageData = NewObject<UPageInfo>();
							Pages.Add(PageId, PageData);
						}
						for (auto Url : Urls)
						{
							auto MediaInfo = AskToDownload(Url);
							PageData->Media.Add(MediaInfo);
							MediaInfo->OnStatusChanged.AddUObject(PageData, &UPageInfo::OnMediaReady);
						}
						PageData->BroadcastStatus();
					}

				}
			}
		}
	});
	Request->SetURL(PortaApiUrl + FString::Printf(TEXT("api/playlists/%d/media?api_key="), Id) + PortaApiKey);
	Request->SetVerb(TEXT("GET"));
	Request->SetHeader(TEXT("User-Agent"), "X-UnrealEngine-Agent");
	Request->SetHeader("Content-Type", TEXT("application/json"));
	Request->ProcessRequest();
}

void UPortaAssetSubsystem::SyncPlaylistStatic(int PlaylistID)
{
	PortaUtils::CallEngineSubsystemMember(&UPortaAssetSubsystem::QueryPlaylistPages, PlaylistID);
}

UMediaInfo* UPortaAssetSubsystem::AskToDownload(const FString& Url)
{
	if (MediaDownloads.Contains(Url))
		return MediaDownloads[Url];

	auto Media = NewObject<UMediaInfo>();
	MediaDownloads.Add(Url, Media);
	Media->Url = Url;

	DownloadAsset(Url);

	return Media;
}

void UPortaAssetSubsystem::DoPlaylistDownloads()
{
	for (auto Url : RequiredUrls)
	{
		IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
		FString FilePath = AssetCacheFolder + LocalPathFromUrl(Url);
		if (!PlatformFile.FileExists(*FilePath))
		{
			auto Request = CreateDownloadRequest(Url);
			Request->OnProcessRequestComplete().BindLambda([this](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully)
			{
				HandleDownloadResponse(Request, Response, bConnectedSuccessfully, nullptr, nullptr, nullptr);
			});
			QueueRequest(Request);
		}
	}
}

void UPortaAssetSubsystem::ListenToStatusChangeStatic(int PageId, FPortaMediaStatusDelegate Callback)
{
	PortaUtils::CallEngineSubsystemMember(&UPortaAssetSubsystem::ListenToStatusChange, PageId, Callback);
}

void UPortaAssetSubsystem::ListenToStatusChange(int PageId, FPortaMediaStatusDelegate Callback)
{
	if (Pages.Contains(PageId))
	{
		if (Callback.IsBound())
		{
			auto Page = Pages[PageId];
			Callback.ExecuteIfBound(Page->GetStatus());
			Page->OnStatusChanged.AddUFunction(Callback.GetUObject(), Callback.GetFunctionName());
		}
	}
	else
		Callback.ExecuteIfBound(EPortaMediaStatus::Ready);
}

EPortaMediaStatus UPortaAssetSubsystem::GetPageStatusStatic(int PageId)
{
	return PortaUtils::CallEngineSubsystemMember(&UPortaAssetSubsystem::GetPageStatus, PageId);
}

EPortaMediaStatus UPortaAssetSubsystem::GetPageStatus(int PageId)
{
	if (Pages.Contains(PageId))
		return Pages[PageId]->GetStatus();
	else
		return EPortaMediaStatus::Ready;
}

void UPortaAssetSubsystem::DownloadAndSetTextureFromPresetStatic(const FString& PresetName, const FString& PropertyName, const FString& Url)
{
	auto subsystem = UPortaAssetSubsystem::Get();
	if (!subsystem)
		return;

	FString FileName = LocalPathFromUrl(Url);

	if (auto Texture = subsystem->Textures.FindRef(FileName))
	{
		SetTextureAssetFromPresetStatic(PresetName, PropertyName, Texture);
	}
	else
	{

		subsystem->DownloadTexture(Url, FAssetDownloadDoneDelegate::CreateLambda([=]()
			{
				SetTextureFromPresetStatic(PresetName, PropertyName, FileName);
			}));
	}
}

void UPortaAssetSubsystem::SetTextureFileFromPresetStatic(const FString& PresetName, const FString& PropertyName, const FString& FilePath)
{
	IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

	if (PlatformFile.FileExists(*FilePath))
	{
		TArray<uint8> RawFileData;
		if (FFileHelper::LoadFileToArray(RawFileData, *FilePath))
		{
			UPortaAssetSubsystem::Get()->LoadImageFromBuffer(FilePath, RawFileData);
			SetTextureFromPresetStatic(PresetName, PropertyName, FilePath);
		}
		else
		{
			UE_LOG(LogPorta, Error, TEXT("Could not load texture file: %s"), *FilePath);
		}
	}
	else
	{
		UE_LOG(LogPorta, Error, TEXT("Texture file not found: %s"), *FilePath);
	}
}

void UPortaAssetSubsystem::SetTextureFromPresetStatic(const FString& PresetName, const FString& PropertyName, const FString& TextureName)
{
	auto Subsystem = UPortaAssetSubsystem::Get();
	if (!Subsystem)
		return;
	auto Texture = Subsystem->Textures.Find(TextureName);
	if (Texture)
		SetTextureAssetFromPresetStatic(PresetName, PropertyName, *Texture);
	else
		UE_LOG(LogPorta, Warning, TEXT("SetTextureFromPresetStatic: Texture not found: %s"), *TextureName);

}

void UPortaAssetSubsystem::SetTextureAssetFromPresetStatic(const FString& PresetName, const FString& PropertyName, UTexture2D* Texture)
{
	bool IsAva;
	FString ChannelName;
	FString Fixed = UPortaConnectionSubsystem::FixupPresetName(PresetName, &IsAva, &ChannelName);
	if (IsAva)
	{
		UPortaAvalancheSubsystem::SetTexture(Fixed, ChannelName, PropertyName, Texture->GetPathName());
		return;
	}

	URemoteControlPreset* Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return;

	TSharedPtr<FRemoteControlProperty> RemoteControlProperty = Preset->GetExposedEntity<FRemoteControlProperty>(Preset->GetExposedEntityId(*PropertyName)).Pin();
	if (!RemoteControlProperty || !RemoteControlProperty->IsBound())
		return;

	auto Object = RemoteControlProperty->GetBoundObject();
	if (!Object)
		return;

	auto Property = CastField<FObjectProperty>(RemoteControlProperty->GetProperty());
	if (Property && Property->PropertyClass == UTexture2D::StaticClass())
	{
		if (UTexture2D** ValPtr = Property->ContainerPtrToValuePtr<UTexture2D*>(Object))
		{
			*ValPtr = Texture;
		}

		return;
	}

	if (Property && Property->PropertyClass == UTexture::StaticClass())
	{
#if 0
		FString Content = TEXT("{\"PropertyValue\":\"") + Texture->GetPathName() + TEXT("\"}");
		UPortaConnectionSubsystem::SetPresetProperty(PresetName, PropertyName, Content);
#else
		if (UTexture** ValPtr = Property->ContainerPtrToValuePtr<UTexture*>(Object))
		{
			*ValPtr = Texture;
		}

#endif

		return;
	}

	UMaterialInstanceDynamic* Material = nullptr;
	if (Property && Property->PropertyClass == UMaterialInstance::StaticClass())
	{
		if (UMaterialInstance** ValPtr = Property->ContainerPtrToValuePtr<UMaterialInstance*>(Object))
		{
			UMaterialInstance* OrigMaterial = *ValPtr;
			Material = Cast<UMaterialInstanceDynamic>(OrigMaterial);
			if (!Material)
			{
				//make our own dynamic instance and set it
				Material = UMaterialInstanceDynamic::Create(OrigMaterial, Object);
				*ValPtr = Material;
			}
		}
	}
	else
	{
		auto MeshComp = Cast<UStaticMeshComponent>(Object);
		if (MeshComp)
		{
			UMaterialInterface* OrigMaterial = nullptr;
			//see if we can just use an existing material
			OrigMaterial = MeshComp->GetMaterial(0);
			if (!OrigMaterial)
				return;
			Material = Cast<UMaterialInstanceDynamic>(OrigMaterial);

			if (!Material)
			{
				//make our own dynamic instance and set it
				Material = UMaterialInstanceDynamic::Create(OrigMaterial, MeshComp);
				MeshComp->SetMaterial(0, Material);
			}
		}
		else
		{
			//for some reason the bound object we get is NOT the Play in Editor version
			//if we're in PIE, we need to grab the PIE version of our object
#if WITH_EDITOR
			if (GIsEditor || GIsPlayInEditorWorld)
			{
				FString PiePath = TEXT("/Game/UEDPIE_0_") + Object->GetPathName().Mid(6);
				Object = StaticFindObject(UObject::StaticClass(), nullptr, *PiePath);
				if (!Object)
					return;
			}
#endif

			Material = Cast<UMaterialInstanceDynamic>(Object);
		}
	}

	if (Material)
		Material->SetTextureParameterValue(TEXT("Param"), Texture);

}

void UPortaAssetSubsystem::DownloadAndCallPresetFuncWithMediaStatic(const FString& PresetName, const FString& FuncName, const TArray<FString>& Args, const TArray<FString>& Urls, const FString& OtherArgs)
{
	auto subsystem = UPortaAssetSubsystem::Get();
	if (!subsystem)
		return;

	if (Args.Num() != Urls.Num())
		return;

	int* NeedsDownload = new int(Urls.Num());
	for (auto DownloadUrl : Urls)
	{
		subsystem->DownloadAsset(DownloadUrl, FAssetDownloadDoneDelegate::CreateLambda([=]()
			{
				if (--(*NeedsDownload) > 0)
					return;

				delete NeedsDownload;

				TSharedPtr<FJsonObject> Params = MakeShareable(new FJsonObject);
				auto ContentReader = TJsonReaderFactory<>::Create(OtherArgs);
				FJsonSerializer::Deserialize(ContentReader, Params);

				for (int i = 0; i < Urls.Num(); ++i)
				{
					FString FileName = LocalPathFromUrl(Urls[i]);
					FString Ext = FPaths::GetExtension(FileName);
					if (IsImageFormat(Ext))
					{
						auto Texture = subsystem->Textures.FindRef(FileName);
						if (!Texture)
							continue;

						Params->SetStringField(Args[i], Texture->GetPathName());
					}
					else if (IsAudioFormat(Ext))
					{
						auto Wave = subsystem->SoundWaves.FindRef(FileName);
						if (!Wave)
							continue;

						Params->SetStringField(Args[i], Wave->GetPathName());
					}
				}

				UPortaConnectionSubsystem::CallPresetFunction(PresetName, FuncName, Params);
			}));
	}

}

void UPortaAssetSubsystem::DownloadAndPlayVideoOnPresetStatic(const FString& PresetName, const FString& PropertyName, const FString& Url)
{
	auto subsystem = UPortaAssetSubsystem::Get();
	if (!subsystem)
		return;

	subsystem->DownloadVideo(Url, FAssetDownloadDoneDelegate::CreateLambda([=]()
		{
			FString FileName = LocalPathFromUrl(Url);
			PlayVideoOnPresetStatic(PresetName, PropertyName, FileName);
		}));
}

void UPortaAssetSubsystem::PlayVideoOnPresetStatic(const FString& PresetName, const FString& PropertyName, const FString& VideoName, bool Uncached)
{
	URemoteControlPreset* Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return;


	TSharedPtr<FRemoteControlProperty> RemoteControlProperty = Preset->GetExposedEntity<FRemoteControlProperty>(Preset->GetExposedEntityId(*PropertyName)).Pin();
	if (!RemoteControlProperty || !RemoteControlProperty->IsBound())
		return;

	auto Object = RemoteControlProperty->GetBoundObject();
	auto ObjectProperty = CastField<FObjectProperty>(RemoteControlProperty->GetProperty());

	if (ObjectProperty && Object)
	{
		UObject* const Value = ObjectProperty->GetObjectPropertyValue_InContainer(Object);
		UMediaPlayer* Player = Cast<UMediaPlayer>(Value);
		if (Player)
		{
			PlayVideoStatic(Player, VideoName, Uncached);
			return;
		}

		UMediaTexture* MediaTexture = Cast<UMediaTexture>(Value);
		if (MediaTexture)
		{
			Player = MediaTexture->GetMediaPlayer();
			if (Player)
			{
				PlayVideoStatic(Player, VideoName, Uncached);
				return;
			}
		}

		if (ObjectProperty->PropertyClass == UTexture::StaticClass())
		{
			if (UTexture** ValPtr = ObjectProperty->ContainerPtrToValuePtr<UTexture*>(Object))
			{
				Player = NewObject<UMediaPlayer>();
				Player->PlayOnOpen = true;
				Player->SetLooping(true);
				auto subsystem = UPortaAssetSubsystem::Get();
				subsystem->MediaPlayers.Add(Player);

				MediaTexture = NewObject<UMediaTexture>();
				MediaTexture->AutoClear = false;
				MediaTexture->SetMediaPlayer(Player);
				MediaTexture->UpdateResource();
				*ValPtr = MediaTexture;

				PlayVideoStatic(Player, VideoName, Uncached);
			}

			return;
		}

	}
}

void UPortaAssetSubsystem::DownloadAndSetAudioOnPresetStatic(const FString& PresetName, const FString& PropertyName, const FString& Url)
{
	auto subsystem = UPortaAssetSubsystem::Get();
	if (!subsystem)
		return;

	subsystem->DownloadAudio(Url, FAssetDownloadDoneDelegate::CreateLambda([=]()
	{
		FString FileName = LocalPathFromUrl(Url);
		auto Wave = subsystem->SoundWaves.FindRef(FileName);
		if (Wave)
		{
			UPortaConnectionSubsystem::Get()->SetPresetProperty(PresetName, PropertyName, TEXT("{ \"PropertyValue\": \"") + Wave->GetPathName() + TEXT("\" }"));
		}
	}));
}

void UPortaAssetSubsystem::SetAudioFileOnPresetStatic(const FString& PresetName, const FString& PropertyName, const FString& FilePath)
{
	auto subsystem = UPortaAssetSubsystem::Get();
	if (!subsystem)
		return;

	subsystem->QueueAudioImport(FilePath, FProcessFileDoneDelegate::CreateLambda([=](bool success) 
	{
		auto Wave = subsystem->SoundWaves.FindRef(FilePath);
		if (Wave)
		{
			PortaUtils::GetEngineSubsystem<UPortaConnectionSubsystem>()->SetPresetProperty(PresetName, PropertyName, TEXT("{ \"PropertyValue\": \"") + Wave->GetPathName() + TEXT("\" }"));
		}
	}));
}

void UPortaAssetSubsystem::SetTextureStatic(UMaterialInstanceDynamic* Material, const FString& TextureName)
{
	PortaUtils::CallEngineSubsystemMember<UPortaAssetSubsystem, UMaterialInstanceDynamic*, const FString&>(&UPortaAssetSubsystem::SetTexture, Material, TextureName);
}

void UPortaAssetSubsystem::PlayVideoStatic(UMediaPlayer* Player, const FString& VideoName, bool Uncached)
{
	PortaUtils::CallEngineSubsystemMember<UPortaAssetSubsystem, UMediaPlayer*, const FString&, bool>(&UPortaAssetSubsystem::PlayVideo, Player, VideoName, Uncached);
}

TArray<FString> UPortaAssetSubsystem::GetLoadedTextureNamesStatic(UObject* WorldContext)
{
	TArray<FString> Keys;
	auto subsystem = UPortaAssetSubsystem::Get();
	if (subsystem)
		subsystem->Textures.GenerateKeyArray(Keys);
	return Keys;
}


void UPortaAssetSubsystem::SetTexture(UMaterialInstanceDynamic* Material, const FString& TextureName)
{
	if (!Material)
		return;
	auto Texture = Textures.Find(TextureName);
	if (Texture)
		Material->SetTextureParameterValue(TEXT("Param"), *Texture);
	else
		UE_LOG(LogPorta, Warning, TEXT("SetTexture: Texture not found: %s"), *TextureName);
}

void UPortaAssetSubsystem::PlayVideo(UMediaPlayer* Player, const FString& VideoName, bool Uncached)
{
	UFileMediaSource* Source = NewObject<UFileMediaSource>();
	if (Uncached)
		Source->FilePath = VideoName;
	else
		Source->FilePath = AssetCacheFolder + VideoName;
	Player->OpenSource(Source);
	Player->SetLooping(true);
}

void UPortaAssetSubsystem::DownloadAssetStatic(const FString& Url)
{
	PortaUtils::CallEngineSubsystemMember<UPortaAssetSubsystem, const FString&>(&UPortaAssetSubsystem::DownloadAsset, Url);
}

void UPortaAssetSubsystem::DownloadAssetsStatic(const TArray<FString>& Urls)
{
	auto subsystem = UPortaAssetSubsystem::Get();
	if (!subsystem)
		return;

	for (auto it : Urls)
	{
		subsystem->DownloadAsset(*it);
	}
}

void UPortaAssetSubsystem::DownloadAsset(const FString& Url)
{
	DownloadAsset(Url, nullptr);
}

void UPortaAssetSubsystem::DownloadAsset(const FString& Url, FAssetDownloadDoneDelegate OnDone)
{
	FString Ext = FPaths::GetExtension(Url);
	if (IsVideoFormat(Ext))
		DownloadVideo(Url, OnDone);
	else if (IsImageFormat(Ext))
		DownloadTexture(Url, OnDone);
	else if (IsAudioFormat(Ext))
		DownloadAudio(Url, OnDone);
	else
		UE_LOG(LogPorta, Error, TEXT("DownloadAsset: Unknown file type for Url: %s"), *Url);
}

void UPortaAssetSubsystem::DownloadTexture(const FString& Url)
{
	DownloadTexture(Url, nullptr);
}

void UPortaAssetSubsystem::DownloadTexture(const FString& Url, FAssetDownloadDoneDelegate OnDone)
{
	AcquireAsset(Url, FProcessDataDelegate::CreateUObject(this, &UPortaAssetSubsystem::LoadImageFromBuffer), nullptr, OnDone);
}

void UPortaAssetSubsystem::DownloadVideo(const FString& Url)
{
	DownloadVideo(Url, nullptr);
}

void UPortaAssetSubsystem::DownloadVideo(const FString& Url, FAssetDownloadDoneDelegate OnDone)
{
	AcquireAsset(Url, nullptr, nullptr, OnDone);
}

void UPortaAssetSubsystem::DownloadAudio(const FString& Url)
{
	DownloadAudio(Url, nullptr);
}

void UPortaAssetSubsystem::DownloadAudio(const FString& Url, FAssetDownloadDoneDelegate OnDone)
{
	AcquireAsset(Url, nullptr, FProcessFileDelegate::CreateUObject(this, &UPortaAssetSubsystem::QueueAudioImport), OnDone);
}

static FString GetLevelFromPresetField(const FRemoteControlField* ExposedField)
{
	for (auto Binding : ExposedField->GetBindings())
	{
		if (auto LevelBinding = Cast<URemoteControlLevelDependantBinding>(Binding.Get()))
		{
			UClass* BindingClass = URemoteControlLevelDependantBinding::StaticClass();
			auto ObjectMapProp = FindFProperty<FMapProperty>(BindingClass, "BoundObjectMapByPath");
			auto KeyProp = CastField<FStructProperty>(ObjectMapProp->GetKeyProperty());
			FScriptMapHelper MapHelper(ObjectMapProp, ObjectMapProp->ContainerPtrToValuePtr<void>(LevelBinding));
			if (MapHelper.Num() > 0)
			{
				FSoftObjectPath SoftObj;
				KeyProp->GetValue_InContainer(MapHelper.GetKeyPtr(0), &SoftObj);
				return SoftObj.GetLongPackageName();
			}
		}
	}

	return FString();
}

FPresetInfo UPortaAssetSubsystem::DescribePreset(const FString& PresetName)
{
	FPresetInfo Info;
	auto Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return Info;

	Info.Name = PresetName;

	auto LevelName = PortaUtils::GetCurrentWorld()->GetPackage()->GetLoadedPath().GetPackageName();

	for (auto Group : Preset->Layout.GetGroups())
	{
		FPresetGroupInfo GroupInfo;
		GroupInfo.Name = Group.Name.ToString();
		for (auto Guid : Group.GetFields())
		{
			auto Field = Preset->GetExposedEntity<FRemoteControlField>(Guid).Pin();
			if (Field.IsValid())
			{
				FPresetFieldInfo FieldInfo;
				FieldInfo.DisplayName = Field->GetLabel().ToString();
				FieldInfo.Level = GetLevelFromPresetField(Field.Get());
				FieldInfo.IsRootLevel = (FieldInfo.Level == LevelName || FieldInfo.Level.IsEmpty());
				GroupInfo.Fields.Add(FieldInfo);
			}
		}
		Info.Groups.Add(GroupInfo);
	}

#if ENGINE_MINOR_VERSION > 0
	for (auto Controller : Preset->GetControllers())
	{
		if (!Controller || !Controller->GetProperty())
			continue;

		FPresetControllerInfo ControllerInfo;
		ControllerInfo.DisplayName = Controller->DisplayName.ToString();
		Info.Controllers.Push(ControllerInfo);
	}
#endif

	return Info;
}

void UPortaAssetSubsystem::GetPresetLevels(const FString& PresetName, TMap<FString, FString>& Levels)
{
	auto Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return;

	for (TWeakPtr<FRemoteControlField> ExposedFieldPtr : Preset->GetExposedEntities<FRemoteControlField>())
	{
		auto ExposedField = ExposedFieldPtr.Pin();
		for (auto Binding : ExposedField->GetBindings())
		{
			if (auto LevelBinding = Cast<URemoteControlLevelDependantBinding>(Binding.Get()))
			{
				UClass* BindingClass = URemoteControlLevelDependantBinding::StaticClass();
				auto ObjectMapProp = FindFProperty<FMapProperty>(BindingClass, "BoundObjectMap");
				auto KeyProp = CastField<FSoftObjectProperty>(ObjectMapProp->GetKeyProperty());

				FScriptMapHelper MapHelper(ObjectMapProp, ObjectMapProp->ContainerPtrToValuePtr<void>(LevelBinding));
				FSoftObjectPtr SoftObj = KeyProp->GetPropertyValue_InContainer(MapHelper.GetKeyPtr(0));
			}

		}
		FString LevelName = GetLevelFromPresetField(ExposedField.Get());
		if (!LevelName.IsEmpty())
			Levels.Add(ExposedField->GetLabel().ToString(), LevelName);
	}
}

FString UPortaAssetSubsystem::GetThumbnail(const FString& AssetPath)
{
#if WITH_EDITOR
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::Get().LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
	FARFilter Filter;
	Filter.SoftObjectPaths.Add(AssetPath);

	TArray<FAssetData> Assets;
	AssetRegistryModule.Get().GetAssets(Filter, Assets);

	if (Assets.Num())
	{
		const FAssetData& AssetData = Assets[0];
		FName ObjectFullName = FName(*AssetData.GetFullName());
		FThumbnailMap ThumbnailMap;
		if (ThumbnailTools::ConditionallyLoadThumbnailsForObjects({ ObjectFullName }, ThumbnailMap))
		{
			if (FObjectThumbnail* Thumbnail = ThumbnailMap.Find(ObjectFullName))
			{
				return FBase64::Encode(Thumbnail->AccessCompressedImageData());
			}
		}

	}
#endif

	return FString();
}

TMap<FString, FString> UPortaAssetSubsystem::GetThumbnails(const TArray<FString>& AssetPaths)
{
	TMap<FString, FString> Ret;

	for (auto AssetPath : AssetPaths)
	{
		Ret.Add(AssetPath, GetThumbnail(AssetPath));
	}

	return Ret;
}


void UPortaAssetSubsystem::SetFlipbookStatic(const FString& PresetName, const FString& PropertyName, UPaperFlipbook* Flipbook)
{
	URemoteControlPreset* Preset = IRemoteControlModule::Get().ResolvePreset(*PresetName);
	if (!Preset)
		return;
	TSharedPtr<FRemoteControlProperty> RemoteControlProperty = Preset->GetExposedEntity<FRemoteControlProperty>(Preset->GetExposedEntityId(*PropertyName)).Pin();
	if (!RemoteControlProperty || !RemoteControlProperty->IsBound())
		return;

	auto Object = RemoteControlProperty->GetBoundObject();
	if (!Object)
		return;

	auto FlipComponent = Cast<UPaperFlipbookComponent>(Object);
	if (!FlipComponent)
		return;

	FlipComponent->SetFlipbook(Flipbook);
}

void UPortaAssetSubsystem::AcquireAsset(const FString& Url, FProcessDataDelegate ProcessData, FProcessFileDelegate ProcessFile, FAssetDownloadDoneDelegate OnDone)
{
	IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
	FString FilePath = AssetCacheFolder + LocalPathFromUrl(Url);

	auto PostCacheProcess = [this, ProcessData, ProcessFile, OnDone, Url](bool success) 
	{
		if (!success)
		{
			auto Request = CreateDownloadRequest(Url);
			Request->OnProcessRequestComplete().BindLambda([this, ProcessData, ProcessFile, OnDone](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully)
				{
					HandleDownloadResponse(Request, Response, bConnectedSuccessfully, ProcessData, ProcessFile, OnDone);
				});
			QueueRequest(Request);
		}
		else
		{
			UMediaInfo* Media = MediaDownloads.FindRef(Url);
			if (Media)
				Media->SetStatus(EPortaMediaStatus::Ready);
			OnDone.ExecuteIfBound();
		}

	};

	if (PlatformFile.FileExists(*FilePath))
	{
		if (ProcessData.IsBound())
		{
			TArray<uint8> RawFileData;
			if (FFileHelper::LoadFileToArray(RawFileData, *FilePath))
			{
				PostCacheProcess(ProcessData.Execute(LocalPathFromUrl(Url), RawFileData));
			}
		}
		else if (ProcessFile.IsBound())
		{
			ProcessFile.Execute(FilePath, FProcessFileDoneDelegate::CreateLambda(PostCacheProcess));
		}
		else
		{
			PostCacheProcess(true);
		}
	}
	else
	{
		PostCacheProcess(false);
	}
}

void UPortaAssetSubsystem::HandleFullAssetSyncResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful)
{
	if (!bWasSuccessful)
	{
		UE_LOG(LogPorta, Error, TEXT("Full Asset Sync failed. Retrying in %f seconds."), AssetSyncRetryTimerSec);
		Retryer = NewObject<UPortaAssetSyncRetryer>(this);
		Retryer->SetTimeUntilRetry(AssetSyncRetryTimerSec);
		return;
	}

	UE_LOG(LogPorta, Log, TEXT("%s"), *Response->GetContentAsString());

	auto ResponseObject = TSharedPtr<FJsonObject>();
	auto Reader = TJsonReaderFactory<TCHAR>::Create(Response->GetContentAsString());
	if (!FJsonSerializer::Deserialize(Reader, ResponseObject))
		return;

	const TArray<TSharedPtr<FJsonValue>>* Data;
	if (ResponseObject->TryGetArrayField(TEXT("data"), Data))
	{
		for (auto Row : *Data)
		{
			const TSharedPtr<FJsonObject>* Playlist;
			if (Row->TryGetObject(Playlist))
			{
				int Id;
				if ((*Playlist)->TryGetNumberField(TEXT("id"), Id))
				{
					QueryPlaylistPages(Id);
				}
			}
		}
	}
}


void UPortaAssetSubsystem::HandleDownloadResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bConnectedSuccessfully, FProcessDataDelegate ProcessData, FProcessFileDelegate ProcessFile, FAssetDownloadDoneDelegate OnDone)
{
	UMediaInfo* Media = MediaDownloads.FindRef(Request->GetURL());
	if (Request.IsValid() && Response.IsValid() && bConnectedSuccessfully && Response->GetResponseCode() == EHttpResponseCodes::Ok)
	{
		FString ResponseString = Response->GetContentAsString();
		TArray<uint8> ContentArray = Response->GetContent();

		FString FilePath = LocalPathFromUrl(Request->GetURL());

		if (!FilePath.IsEmpty())
		{
			SaveFile(FilePath, ContentArray);
			if (ProcessData.IsBound())
			{
				if (!ProcessData.Execute(FilePath, ContentArray))
				{
					if (Media)
						Media->SetStatus(EPortaMediaStatus::Error);
					GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, TEXT("UPortaAssetSubsystem::HandleDownloadResponse - file processing failed"));
					UE_LOG(LogPorta, Error, TEXT("File processing failed. URL: %s"), *Response->GetURL());
					OnDone.ExecuteIfBound();
				}
				else
				{
				
#if 0
					//For testing. This should not be active
					FTimerHandle Timer;
					GetWorld()->GetTimerManager().SetTimer(Timer, FTimerDelegate::CreateLambda([Media] {
						if (Media)
							Media->SetStatus(EPortaMediaStatus::Ready);

					}), 10.f, false);

#else
					if (Media)
						Media->SetStatus(EPortaMediaStatus::Ready);
#endif
					OnDone.ExecuteIfBound();

				}
			}
			else if (ProcessFile.IsBound())
			{
				ProcessFile.Execute(FilePath, FProcessFileDoneDelegate::CreateLambda([=](bool success) {
					if (Media)
						Media->SetStatus(success ? EPortaMediaStatus::Ready : EPortaMediaStatus::Error);
					OnDone.ExecuteIfBound();
				}));
			}
			else
			{
				if (Media)
					Media->SetStatus(EPortaMediaStatus::Ready);
				OnDone.ExecuteIfBound();
			}
		}
	}
	else
	{
		if (Media)
			Media->SetStatus(EPortaMediaStatus::Error);
		GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Red, TEXT("UPortaAssetSubsystem::HandleDownloadResponse - Request Failed"));
		if (Response.IsValid())
		{
			UE_LOG(LogPorta, Error, TEXT("ResponseCode: %d"), Response->GetResponseCode());
			UE_LOG(LogPorta, Error, TEXT("URL: %s"), *Response->GetURL());
		}
		OnDone.ExecuteIfBound();
	}

	DequeueRequest();
}

bool UPortaAssetSubsystem::SaveFile(const FString& FileName, const TArray<uint8>& Content)
{
	IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();

	if (PlatformFile.CreateDirectoryTree(*AssetCacheFolder))
	{
		FString FilePath = AssetCacheFolder + FileName;
		return FFileHelper::SaveArrayToFile(Content, *FilePath);
	}
	else
	{
		return false;
	}
}

bool UPortaAssetSubsystem::LoadImageFromBuffer(const FString& Name, const TArray<uint8>& RawFileData)
{
	EImageFormat Format = ImageFormatFromExt(FPaths::GetExtension(Name));

	IImageWrapperModule& ImageWrapperModule = FModuleManager::LoadModuleChecked<IImageWrapperModule>(FName("ImageWrapper"));
	TSharedPtr<IImageWrapper> ImageWrapper = ImageWrapperModule.CreateImageWrapper(Format);

	if (ImageWrapper.IsValid() && ImageWrapper->SetCompressed(RawFileData.GetData(), RawFileData.Num()))
	{
		TArray<uint8> UncompressedBGRA;
		if (ImageWrapper->GetRaw(ERGBFormat::BGRA, 8, UncompressedBGRA))
		{
			auto Texture = UTexture2D::CreateTransient(ImageWrapper->GetWidth(), ImageWrapper->GetHeight(), PF_B8G8R8A8);

			if (!Texture)
				return false;

			void* TextureData = Texture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_WRITE);
			FMemory::Memcpy(TextureData, UncompressedBGRA.GetData(), UncompressedBGRA.Num());
			Texture->GetPlatformData()->Mips[0].BulkData.Unlock();

			Texture->UpdateResource();
			Textures.Add(Name, Texture);
			return true;
		}
	}
	return false;
}

TSharedRef<IHttpRequest, ESPMode::ThreadSafe> UPortaAssetSubsystem::CreateDownloadRequest(const FString& Url)
{
	TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
	Request->SetURL(Url);
	Request->SetVerb("GET");
	//Request->SetHeader("Authorization", FString::Printf(TEXT("Bearer %s"), *ApiKey));
	Request->SetHeader("Accept", TEXT("*/*"));
	return Request;
}

void UPortaAssetSubsystem::QueueRequest(TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request)
{
	if (AvailableRequests > 0)
	{
		--AvailableRequests;
		Request->ProcessRequest();
	}
	else
	{
		PendingRequests.Add(Request);
	}
}

void UPortaAssetSubsystem::DequeueRequest()
{
	if (PendingRequests.Num() == 0)
	{
		++AvailableRequests;
	}
	else
	{
		auto Request = PendingRequests[0];
		PendingRequests.RemoveAt(0);
		Request->ProcessRequest();
	}
}

void UPortaAssetSubsystem::OnAudioImportResult(URuntimeAudioImporterLibrary* AudioLib, UImportedSoundWave* Wave, ERuntimeImportStatus Status)
{
	auto Info = PendingAudioImports[0];
	PendingAudioImports.RemoveAt(0);

	if (Status == ERuntimeImportStatus::SuccessfulImport)
	{
		SoundWaves.Add(Info.FilePath, Wave);
	}

	Info.OnDone.ExecuteIfBound(Status == ERuntimeImportStatus::SuccessfulImport);

	ProcessNextAudioImport();
}

void UPortaAssetSubsystem::QueueAudioImport(const FString& FilePath, FProcessFileDoneDelegate OnDone)
{
	PendingAudioImports.Add(FAudioImportInfo{FilePath, OnDone});
	if (PendingAudioImports.Num() == 1)
		ProcessNextAudioImport();
}

void UPortaAssetSubsystem::ProcessNextAudioImport()
{
	if (PendingAudioImports.Num() == 0)
		return;
	auto& Info = PendingAudioImports[0];
	AudioLibrary->ImportAudioFromFile(Info.FilePath, ERuntimeAudioFormat::Auto);
}

FString UPortaAssetSubsystem::LocalPathFromUrl(const FString& Url)
{
	FString Working(Url);
	int index = Url.Find(TEXT("://"));
	if (index > -1)
		Working = Working.Mid(index + 3);

	index = Working.Find(TEXT("/"));
	if (index > -1)
		return Working.Mid(index + 1);
	else
		return Working;
}

bool UPortaAssetSubsystem::IsImageFormat(const FString& Ext)
{
	FString Lower = Ext.ToLower();
	return Lower == TEXT("jpg") || Lower == TEXT("jpeg") || Lower == TEXT("png") || Lower == TEXT("bmp") || Lower == TEXT("tiff") || Lower == TEXT("tif");
}

EImageFormat UPortaAssetSubsystem::ImageFormatFromExt(const FString& Ext)
{
	FString Lower = Ext.ToLower();
	if (Lower == TEXT("jpg") || Lower == TEXT("jpeg"))
		return EImageFormat::JPEG;
	if (Lower == TEXT("png"))
		return EImageFormat::PNG;
	if (Lower == TEXT("bmp"))
		return EImageFormat::BMP;
	if (Lower == TEXT("tiff") || Lower == TEXT("tif"))
		return EImageFormat::TIFF;

	UE_LOG(LogPorta, Error, TEXT("Unknown Image Format! %s"), *Ext);
	return EImageFormat::Invalid;
}

bool UPortaAssetSubsystem::IsVideoFormat(const FString& Ext)
{
	FString Lower = Ext.ToLower();
	return Lower == TEXT("mp4") || Lower == TEXT("mpeg") || Lower == TEXT("mpg") || Lower == TEXT("avi") || Lower == TEXT("mov") || Lower == TEXT("m4v");
}

bool UPortaAssetSubsystem::IsAudioFormat(const FString& Ext)
{
	FString Lower = Ext.ToLower();
	return Lower == TEXT("mp3") || Lower == TEXT("wav");
}
