// @lint-ignore-every LICENSELINT
// Copyright Epic Games, Inc. All Rights Reserved.

#include "OculusXRSceneActor.h"
#include "OculusXRSceneModule.h"
#include "OculusXRHMDModule.h"
#include "OculusXRHMD.h"
#include "OculusXRAnchorManager.h"
#include "OculusXRAnchorTypes.h"
#include "OculusXRAnchorBPFunctionLibrary.h"
#include "OculusXRSceneDelegates.h"
#include "OculusXRDelegates.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/AssetManager.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "Engine/World.h"
#include "GameFramework/WorldSettings.h"
#include "ProceduralMeshComponent.h"
#include "OculusXRSceneGlobalMeshComponent.h"

#define LOCTEXT_NAMESPACE "OculusXRSceneActor"

//////////////////////////////////////////////////////////////////////////
// ASceneActor

AOculusXRSceneActor::AOculusXRSceneActor(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	ResetStates();

	// Create required components
	RoomLayoutManagerComponent = CreateDefaultSubobject<UOculusXRRoomLayoutManagerComponent>(TEXT("OculusXRRoomLayoutManagerComponent"));

	// Following are the semantic labels we want to support default properties for.  User can always add new ones through the properties panel if needed.
	const FString default2DSemanticClassifications[] = {
		TEXT("WALL_FACE"),
		TEXT("CEILING"),
		TEXT("FLOOR"),
		TEXT("COUCH"),
		TEXT("TABLE"),
		TEXT("DOOR_FRAME"),
		TEXT("WINDOW_FRAME"),
		TEXT("WALL_ART"),
		TEXT("INVISIBLE_WALL_FACE"),
		TEXT("OTHER")
	};

	const FString default3DSemanticClassifications[] = {
		TEXT("COUCH"),
		TEXT("TABLE"),
		TEXT("SCREEN"),
		TEXT("BED"),
		TEXT("LAMP"),
		TEXT("PLANT"),
		TEXT("STORAGE"),
		TEXT("OTHER")
	};

	FOculusXRSpawnedSceneAnchorProperties spawnedAnchorProps;
	spawnedAnchorProps.ActorComponent = nullptr;
	spawnedAnchorProps.StaticMesh = nullptr;

	// Setup initial scene plane and volume properties
	for (auto& semanticLabel2D : default2DSemanticClassifications)
	{
		FOculusXRSpawnedSceneAnchorProperties& props = ScenePlaneSpawnedSceneAnchorProperties.Add(semanticLabel2D, spawnedAnchorProps);
		props.ForceParallelToFloor = (semanticLabel2D != "WALL_FACE");
	}

	for (auto& semanticLabel3D : default3DSemanticClassifications)
	{
		FOculusXRSpawnedSceneAnchorProperties& props = SceneVolumeSpawnedSceneAnchorProperties.Add(semanticLabel3D, spawnedAnchorProps);
		props.ForceParallelToFloor = true;
	}
}

void AOculusXRSceneActor::ResetStates()
{
	bCaptureFlowWasLaunched = false;
	ClearScene();
}

void AOculusXRSceneActor::BeginPlay()
{
	Super::BeginPlay();

	// Create a scene component as root so we can attach spawned actors to it
	USceneComponent* rootSceneComponent = NewObject<USceneComponent>(this, USceneComponent::StaticClass());
	rootSceneComponent->SetMobility(EComponentMobility::Static);
	rootSceneComponent->RegisterComponent();
	SetRootComponent(rootSceneComponent);

	SceneGlobalMeshComponent = FindComponentByClass<UOculusXRSceneGlobalMeshComponent>();

	// Register delegates
	RoomLayoutManagerComponent->OculusXRRoomLayoutSceneCaptureCompleteNative.AddUObject(this, &AOculusXRSceneActor::SceneCaptureComplete_Handler);

	// Make an initial request to query for the room layout if bPopulateSceneOnBeginPlay was set to true
	if (bPopulateSceneOnBeginPlay)
	{
		PopulateScene();
	}

}

void AOculusXRSceneActor::EndPlay(EEndPlayReason::Type Reason)
{
	// Unregister delegates
	RoomLayoutManagerComponent->OculusXRRoomLayoutSceneCaptureCompleteNative.RemoveAll(this);

	// Calling ResetStates will reset member variables to their default values (including the request IDs).
	ResetStates();


	Super::EndPlay(Reason);
}

void AOculusXRSceneActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
}

bool AOculusXRSceneActor::IsValidUuid(const FOculusXRUUID& Uuid)
{
	return Uuid.UUIDBytes != nullptr;
}

void AOculusXRSceneActor::LaunchCaptureFlow()
{
	UE_LOG(LogOculusXRScene, Verbose, TEXT("Launch capture flow"));

	if (RoomLayoutManagerComponent)
	{
		UE_LOG(LogOculusXRScene, Verbose, TEXT("Launch capture flow -- RoomLayoutManagerComponent"));

		const bool bResult = RoomLayoutManagerComponent->LaunchCaptureFlow();
		if (!bResult)
		{
			UE_LOG(LogOculusXRScene, Error, TEXT("LaunchCaptureFlow() failed!"));
		}
	}
}

void AOculusXRSceneActor::LaunchCaptureFlowIfNeeded()
{
#if WITH_EDITOR
	UE_LOG(LogOculusXRScene, Display, TEXT("Scene Capture does not work over Link. Please capture a scene with the HMD in standalone mode, then access the scene model over Link."));
#else
	// Depending on LauchCaptureFlowWhenMissingScene, we might not want to launch Capture Flow
	if (LauchCaptureFlowWhenMissingScene != EOculusXRLaunchCaptureFlowWhenMissingScene::NEVER)
	{
		if (LauchCaptureFlowWhenMissingScene == EOculusXRLaunchCaptureFlowWhenMissingScene::ALWAYS || (!bCaptureFlowWasLaunched && LauchCaptureFlowWhenMissingScene == EOculusXRLaunchCaptureFlowWhenMissingScene::ONCE))
		{
			LaunchCaptureFlow();
		}
	}
#endif
}

AActor* AOculusXRSceneActor::SpawnActorWithSceneComponent(const FOculusXRUInt64& Space, const FOculusXRUInt64& RoomSpaceID, const TArray<FString>& SemanticClassifications, UClass* sceneAnchorComponentInstanceClass)
{
	FActorSpawnParameters actorSpawnParams;
	actorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
	AActor* Anchor = GetWorld()->SpawnActor<AActor>(AActor::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, actorSpawnParams);

	USceneComponent* rootComponent = NewObject<USceneComponent>(Anchor, USceneComponent::StaticClass());
	rootComponent->SetMobility(EComponentMobility::Movable);
	rootComponent->RegisterComponent();
	Anchor->SetRootComponent(rootComponent);
	rootComponent->SetWorldLocation(FVector::ZeroVector);

	Anchor->AttachToActor(this, FAttachmentTransformRules::KeepRelativeTransform);

#if WITH_EDITOR
	if (SemanticClassifications.Num() > 0)
	{
		Anchor->SetActorLabel(FString::Join(SemanticClassifications, TEXT("-")), false);
	}
#endif

	UOculusXRSceneAnchorComponent* sceneAnchorComponent = NewObject<UOculusXRSceneAnchorComponent>(Anchor, sceneAnchorComponentInstanceClass);
	sceneAnchorComponent->RegisterComponent();

	sceneAnchorComponent->SetHandle(Space);
	sceneAnchorComponent->SemanticClassifications = SemanticClassifications;
	sceneAnchorComponent->RoomSpaceID = RoomSpaceID;

	EOculusXRAnchorResult::Type Result;
	OculusXRAnchors::FOculusXRAnchors::SetAnchorComponentStatus(sceneAnchorComponent, EOculusXRSpaceComponentType::Locatable, true, 0.0f, FOculusXRAnchorSetComponentStatusDelegate(), Result);

	return Anchor;
}

AActor* AOculusXRSceneActor::SpawnOrUpdateSceneAnchor(AActor* Anchor, const FOculusXRUInt64& Space, const FOculusXRUInt64& RoomSpaceID, const FVector& BoundedPos, const FVector& BoundedSize, const TArray<FString>& SemanticClassifications, const EOculusXRSpaceComponentType AnchorComponentType)
{
	if (Space.Value == 0)
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("AOculusXRSceneActor::SpawnOrUpdateSceneAnchor Invalid Space handle."));
		return Anchor;
	}

	if (!(AnchorComponentType == EOculusXRSpaceComponentType::ScenePlane || AnchorComponentType == EOculusXRSpaceComponentType::SceneVolume))
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("AOculusXRSceneActor::SpawnOrUpdateSceneAnchor Anchor doesn't have ScenePlane or SceneVolume component active."));
		return Anchor;
	}

	if (0 == SemanticClassifications.Num())
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("AOculusXRSceneActor::SpawnOrUpdateSceneAnchor No semantic classification found."));
		return Anchor;
	}

	FOculusXRSpawnedSceneAnchorProperties* foundProperties = (AnchorComponentType == EOculusXRSpaceComponentType::ScenePlane) ? ScenePlaneSpawnedSceneAnchorProperties.Find(SemanticClassifications[0]) : SceneVolumeSpawnedSceneAnchorProperties.Find(SemanticClassifications[0]);

	if (!foundProperties)
	{
		UE_LOG(LogOculusXRScene, Warning, TEXT("AOculusXRSceneActor::SpawnOrUpdateSceneAnchor Scene object has an unknown semantic label.  Will not be spawned."));
		return Anchor;
	}

	TSoftClassPtr<UOculusXRSceneAnchorComponent>* sceneAnchorComponentClassPtrRef = &foundProperties->ActorComponent;
	TSoftObjectPtr<UStaticMesh>* staticMeshObjPtrRef = &foundProperties->StaticMesh;

	UClass* sceneAnchorComponentInstanceClass = sceneAnchorComponentClassPtrRef->LoadSynchronous();
	if (!sceneAnchorComponentInstanceClass)
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("AOculusXRSceneActor::SpawnOrUpdateSceneAnchor Scene anchor component class is invalid!  Cannot spawn actor to populate the scene."));
		return Anchor;
	}

	if (!Anchor)
	{
		Anchor = SpawnActorWithSceneComponent(Space, RoomSpaceID, SemanticClassifications, sceneAnchorComponentInstanceClass);
	}

	if (staticMeshObjPtrRef && staticMeshObjPtrRef->IsPending())
	{
		staticMeshObjPtrRef->LoadSynchronous();
	}
	UStaticMesh* refStaticMesh = staticMeshObjPtrRef ? staticMeshObjPtrRef->Get() : nullptr;
	if (refStaticMesh == nullptr)
	{
		UE_LOG(LogOculusXRScene, Warning, TEXT("AOculusXRSceneActor::SpawnOrUpdateSceneAnchor Spawn scene anchor mesh is invalid for %s!"), *SemanticClassifications[0]);
		return Anchor;
	}

	UStaticMeshComponent* staticMeshComponent = NewObject<UStaticMeshComponent>(Anchor, UStaticMeshComponent::StaticClass());
	staticMeshComponent->RegisterComponent();
	staticMeshComponent->SetStaticMesh(refStaticMesh);
	staticMeshComponent->AttachToComponent(Anchor->GetRootComponent(), FAttachmentTransformRules::KeepWorldTransform);
	const float worldToMeters = GetWorld()->GetWorldSettings()->WorldToMeters;
	FVector offset(0.0f, BoundedSize.Y / 2.0f, BoundedSize.Z / 2.0f);
	staticMeshComponent->SetRelativeLocation(foundProperties->AddOffset + ((BoundedPos + offset) * worldToMeters), false, nullptr, ETeleportType::ResetPhysics);

	// Setup scale based on bounded size and the actual size of the mesh
	UStaticMesh* staticMesh = staticMeshComponent->GetStaticMesh();
	FBoxSphereBounds staticMeshBounds;
	staticMeshBounds.BoxExtent = FVector{ 1.f, 1.f, 1.f };
	if (staticMesh)
	{
		staticMeshBounds = staticMesh->GetBounds();
	}

	staticMeshComponent->SetRelativeScale3D(FVector(
		(BoundedSize.X < UE_SMALL_NUMBER) ? 1 : (BoundedSize.X / (staticMeshBounds.BoxExtent.X * 2.f)) * worldToMeters,
		(BoundedSize.Y < UE_SMALL_NUMBER) ? 1 : (BoundedSize.Y / (staticMeshBounds.BoxExtent.Y * 2.f)) * worldToMeters,
		(BoundedSize.Z < UE_SMALL_NUMBER) ? 1 : (BoundedSize.Z / (staticMeshBounds.BoxExtent.Z * 2.f)) * worldToMeters));

	return Anchor;
}


bool AOculusXRSceneActor::IsScenePopulated()
{
	if (!RootComponent)
	{
		return false;
	}

	return RootComponent->GetNumChildrenComponents() > 0;
}

bool AOculusXRSceneActor::IsRoomLayoutValid()
{
	return true;
}

void AOculusXRSceneActor::PopulateScene()
{
	if (!RootComponent)
	{
		return;
	}

	const EOculusXRAnchorResult::Type result = QueryAllRooms();
	if (!UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(result))
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("PopulateScene Failed to query available rooms"));
	}
}

void AOculusXRSceneActor::ClearScene()
{
	if (!RootComponent)
		return;

	TArray<USceneComponent*> childrenComponents = RootComponent->GetAttachChildren();
	for (USceneComponent* SceneComponent : childrenComponents)
	{
		Cast<AActor>(SceneComponent->GetOuter())->Destroy();
	}

	bRoomLayoutIsValid = false;
	bFoundCapturedScene = false;
}

void AOculusXRSceneActor::SetVisibilityToAllSceneAnchors(const bool bIsVisible)
{
	if (!RootComponent)
		return;

	TArray<USceneComponent*> childrenComponents = RootComponent->GetAttachChildren();
	for (USceneComponent* sceneComponent : childrenComponents)
	{
		sceneComponent->SetVisibility(bIsVisible, true);
	}
}

void AOculusXRSceneActor::SetVisibilityToSceneAnchorsBySemanticLabel(const FString SemanticLabel, const bool bIsVisible)
{
	FString label = SemanticLabel;
	if (SemanticLabel == TEXT("DESK"))
	{
		label = TEXT("TABLE");
		UE_LOG(LogOculusXRScene, Warning, TEXT("XR Scene Actor semantic lable 'DESK' is deprecated, use 'TABLE' instead."));
	}

	if (!RootComponent)
		return;

	TArray<USceneComponent*> childrenComponents = RootComponent->GetAttachChildren();
	for (USceneComponent* sceneComponent : childrenComponents)
	{
		UObject* outerObject = sceneComponent->GetOuter();
		if (!outerObject)
		{
			continue;
		}

		AActor* outerActor = Cast<AActor>(outerObject);
		if (!outerActor)
		{
			continue;
		}

		UActorComponent* sceneAnchorComponent = outerActor->GetComponentByClass(UOculusXRSceneAnchorComponent::StaticClass());
		if (!sceneAnchorComponent)
		{
			continue;
		}

		if (Cast<UOculusXRSceneAnchorComponent>(sceneAnchorComponent)->SemanticClassifications.Contains(label))
		{
			sceneComponent->SetVisibility(bIsVisible, true);
		}
	}
}

TArray<AActor*> AOculusXRSceneActor::GetActorsBySemanticLabel(const FString SemanticLabel)
{
	FString label = SemanticLabel;
	if (SemanticLabel == TEXT("DESK"))
	{
		label = TEXT("TABLE");
		UE_LOG(LogOculusXRScene, Warning, TEXT("XR Scene Actor semantic lable 'DESK' is deprecated, use 'TABLE' instead."));
	}

	TArray<AActor*> actors;

	if (!RootComponent)
		return actors;

	TArray<USceneComponent*> childrenComponents = RootComponent->GetAttachChildren();
	for (USceneComponent* sceneComponent : childrenComponents)
	{
		UObject* outerObject = sceneComponent->GetOuter();
		if (!outerObject)
		{
			continue;
		}

		AActor* outerActor = Cast<AActor>(outerObject);
		if (!outerActor)
		{
			continue;
		}

		UActorComponent* sceneAnchorComponent = outerActor->GetComponentByClass(UOculusXRSceneAnchorComponent::StaticClass());
		if (!sceneAnchorComponent)
		{
			continue;
		}

		if (Cast<UOculusXRSceneAnchorComponent>(sceneAnchorComponent)->SemanticClassifications.Contains(label))
		{
			actors.Add(outerActor);
		}
	}

	return actors;
}

TArray<FOculusXRRoomLayout> AOculusXRSceneActor::GetRoomLayouts() const
{
	TArray<FOculusXRRoomLayout> layouts;
	RoomLayouts.GenerateValueArray(layouts);
	return layouts;
}

EOculusXRAnchorResult::Type AOculusXRSceneActor::QueryAllRooms()
{
	FOculusXRSpaceQueryInfo queryInfo;
	queryInfo.MaxQuerySpaces = MaxQueries;
	queryInfo.FilterType = EOculusXRSpaceQueryFilterType::FilterByComponentType;
	queryInfo.ComponentFilter.Add(EOculusXRSpaceComponentType::RoomLayout);

	EOculusXRAnchorResult::Type anchorQueryResult;
	OculusXRAnchors::FOculusXRAnchors::QueryAnchorsAdvanced(queryInfo,
		FOculusXRAnchorQueryDelegate::CreateUObject(this, &AOculusXRSceneActor::RoomLayoutQueryComplete), anchorQueryResult);

	return anchorQueryResult;
}

void AOculusXRSceneActor::RoomLayoutQueryComplete(EOculusXRAnchorResult::Type AnchorResult, const TArray<FOculusXRSpaceQueryResult>& QueryResults)
{
	UE_LOG(LogOculusXRScene, Verbose, TEXT("RoomLayoutQueryComplete (Result = %d)"), AnchorResult);

	for (auto& QueryElement : QueryResults)
	{
		UE_LOG(LogOculusXRScene, Verbose, TEXT("RoomLayoutQueryComplete -- Query Element (space = %llu, uuid = %s"), QueryElement.Space.Value, *BytesToHex(QueryElement.UUID.UUIDBytes, OCULUSXR_UUID_SIZE));

		FOculusXRRoomLayout roomLayout;
		const bool bGetRoomLayoutResult = RoomLayoutManagerComponent->GetRoomLayout(QueryElement.Space.Value, roomLayout, MaxQueries);
		if (!bGetRoomLayoutResult)
		{
			UE_LOG(LogOculusXRScene, Error, TEXT("RoomLayoutQueryComplete -- Failed to get room layout for space (space = %llu, uuid = %s"),
				QueryElement.Space.Value, *BytesToHex(QueryElement.UUID.UUIDBytes, OCULUSXR_UUID_SIZE));
			continue;
		}

		roomLayout.RoomAnchorHandle = QueryElement.Space;
		roomLayout.RoomUuid = QueryElement.UUID;


		// If we're only loading the active room we start that floor check query here, otherwise do the room query
		if (bActiveRoomOnly)
		{
			QueryFloorForActiveRoom(QueryElement.Space, roomLayout);
		}
		else
		{
			StartSingleRoomQuery(QueryElement.Space, roomLayout);
		}
	}
}

EOculusXRAnchorResult::Type AOculusXRSceneActor::QueryRoomUUIDs(const FOculusXRUInt64 RoomSpaceID, const TArray<FOculusXRUUID>& RoomUUIDs)
{
	EOculusXRAnchorResult::Type startAnchorQueryResult;
	OculusXRAnchors::FOculusXRAnchors::QueryAnchors(
		RoomUUIDs,
		EOculusXRSpaceStorageLocation::Local,
		FOculusXRAnchorQueryDelegate::CreateUObject(this, &AOculusXRSceneActor::SceneRoomQueryComplete, RoomSpaceID),
		startAnchorQueryResult);

	return startAnchorQueryResult;
}

void AOculusXRSceneActor::SceneRoomQueryComplete(EOculusXRAnchorResult::Type AnchorResult, const TArray<FOculusXRSpaceQueryResult>& QueryResults, const FOculusXRUInt64 RoomSpaceID)
{
	if (!UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(AnchorResult))
	{
		return;
	}

	bool bOutPending = false;
	for (auto& AnchorQueryElement : QueryResults)
	{
		if (SceneGlobalMeshComponent)
		{
			TArray<FString> semanticClassifications;
			GetSemanticClassifications(AnchorQueryElement.Space.Value, semanticClassifications);
			UE_LOG(LogOculusXRScene, Log, TEXT("SpatialAnchor Scene label is %s"), semanticClassifications.Num() > 0 ? *semanticClassifications[0] : TEXT("unknown"));
			if (semanticClassifications.Contains(UOculusXRSceneGlobalMeshComponent::GlobalMeshSemanticLabel))
			{
				bool bIsTriangleMesh = false;
				EOculusXRAnchorResult::Type result = OculusXRAnchors::FOculusXRAnchorManager::GetSpaceComponentStatus(
					AnchorQueryElement.Space.Value, EOculusXRSpaceComponentType::TriangleMesh, bIsTriangleMesh, bOutPending);

				if (!UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(result) || !bIsTriangleMesh)
				{
					UE_LOG(LogOculusXRScene, Error, TEXT("SpatialAnchorQueryResult_Handler Failed to load Triangle Mesh Component for a GLOBAL_MESH"));
					continue;
				}

				UClass* sceneAnchorComponentInstanceClass = SceneGlobalMeshComponent->GetAnchorComponentClass();

				AActor* globalMeshAnchor = SpawnActorWithSceneComponent(AnchorQueryElement.Space.Value, RoomSpaceID, semanticClassifications, sceneAnchorComponentInstanceClass);

				SceneGlobalMeshComponent->CreateMeshComponent(AnchorQueryElement.Space, globalMeshAnchor, RoomLayoutManagerComponent);
				continue;
			}
		}

		bool bIsScenePlane = false;
		bool bIsSceneVolume = false;
		EOculusXRAnchorResult::Type isPlaneResult = OculusXRAnchors::FOculusXRAnchorManager::GetSpaceComponentStatus(
			AnchorQueryElement.Space.Value, EOculusXRSpaceComponentType::ScenePlane, bIsScenePlane, bOutPending);

		EOculusXRAnchorResult::Type isVolumeResult = OculusXRAnchors::FOculusXRAnchorManager::GetSpaceComponentStatus(
			AnchorQueryElement.Space.Value, EOculusXRSpaceComponentType::SceneVolume, bIsSceneVolume, bOutPending);

		bool bIsPlaneResultSuccess = UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(isPlaneResult);
		bool bIsVolumeResultSuccess = UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(isVolumeResult);

		AActor* anchor = nullptr;

		if (bIsPlaneResultSuccess && bIsScenePlane)
		{
			EOculusXRAnchorResult::Type Result;
			FVector scenePlanePos;
			FVector scenePlaneSize;
			bool ResultSuccess = OculusXRAnchors::FOculusXRAnchors::GetSpaceScenePlane(AnchorQueryElement.Space.Value, scenePlanePos, scenePlaneSize, Result);
			if (ResultSuccess)
			{
				UE_LOG(LogOculusXRScene, Log, TEXT("SpatialAnchorQueryResult_Handler ScenePlane pos = [%.2f, %.2f, %.2f], size = [%.2f, %.2f, %.2f]."),
					scenePlanePos.X, scenePlanePos.Y, scenePlanePos.Z,
					scenePlaneSize.X, scenePlaneSize.Y, scenePlaneSize.Z);

				TArray<FString> semanticClassifications;
				GetSemanticClassifications(AnchorQueryElement.Space.Value, semanticClassifications);

				UE_LOG(LogOculusXRScene, Log, TEXT("SpatialAnchor ScenePlane label is %s"), semanticClassifications.Num() > 0 ? *semanticClassifications[0] : TEXT("unknown"));

				anchor = SpawnOrUpdateSceneAnchor(anchor, AnchorQueryElement.Space, RoomSpaceID, scenePlanePos, scenePlaneSize, semanticClassifications, EOculusXRSpaceComponentType::ScenePlane);
				if (!anchor)
				{
					UE_LOG(LogOculusXRScene, Error, TEXT("SpatialAnchorQueryResult_Handler Failed to spawn scene anchor."));
				}
			}
			else
			{
				UE_LOG(LogOculusXRScene, Error, TEXT("SpatialAnchorQueryResult_Handler Failed to get bounds for ScenePlane space."));
			}
		}

		if (bIsVolumeResultSuccess && bIsSceneVolume)
		{
			EOculusXRAnchorResult::Type Result;
			FVector sceneVolumePos;
			FVector sceneVolumeSize;
			bool ResultSuccess = OculusXRAnchors::FOculusXRAnchors::GetSpaceSceneVolume(AnchorQueryElement.Space.Value, sceneVolumePos, sceneVolumeSize, Result);
			if (ResultSuccess)
			{
				UE_LOG(LogOculusXRScene, Log, TEXT("SpatialAnchorQueryResult_Handler SceneVolume pos = [%.2f, %.2f, %.2f], size = [%.2f, %.2f, %.2f]."),
					sceneVolumePos.X, sceneVolumePos.Y, sceneVolumePos.Z,
					sceneVolumeSize.X, sceneVolumeSize.Y, sceneVolumeSize.Z);

				TArray<FString> semanticClassifications;
				GetSemanticClassifications(AnchorQueryElement.Space.Value, semanticClassifications);

				UE_LOG(LogOculusXRScene, Log, TEXT("SpatialAnchor SceneVolume label is %s"), semanticClassifications.Num() > 0 ? *semanticClassifications[0] : TEXT("unknown"));

				anchor = SpawnOrUpdateSceneAnchor(anchor, AnchorQueryElement.Space, RoomSpaceID, sceneVolumePos, sceneVolumeSize, semanticClassifications, EOculusXRSpaceComponentType::SceneVolume);
				if (!anchor)
				{
					UE_LOG(LogOculusXRScene, Error, TEXT("SpatialAnchorQueryResult_Handler Failed to spawn scene anchor."));
				}
			}
			else
			{
				UE_LOG(LogOculusXRScene, Error, TEXT("SpatialAnchorQueryResult_Handler Failed to get bounds for SceneVolume space."));
			}
		}
	}
}

void AOculusXRSceneActor::StartSingleRoomQuery(FOculusXRUInt64 RoomSpaceID, FOculusXRRoomLayout RoomLayout)
{
	EOculusXRAnchorResult::Type startQueryResult = QueryRoomUUIDs(RoomSpaceID, RoomLayout.RoomObjectUUIDs);
	if (UOculusXRAnchorBPFunctionLibrary::IsAnchorResultSuccess(startQueryResult))
	{
		RoomLayouts.Add(RoomSpaceID, std::move(RoomLayout));
	}
}

EOculusXRAnchorResult::Type AOculusXRSceneActor::QueryFloorForActiveRoom(FOculusXRUInt64 RoomSpaceID, FOculusXRRoomLayout RoomLayout)
{
	EOculusXRAnchorResult::Type anchorQueryResult;
	OculusXRAnchors::FOculusXRAnchors::QueryAnchors(
		TArray<FOculusXRUUID>({ RoomLayout.FloorUuid }),
		EOculusXRSpaceStorageLocation::Local,
		FOculusXRAnchorQueryDelegate::CreateUObject(this, &AOculusXRSceneActor::ActiveRoomFloorQueryComplete, RoomSpaceID, RoomLayout),
		anchorQueryResult);

	return anchorQueryResult;
}

void AOculusXRSceneActor::ActiveRoomFloorQueryComplete(EOculusXRAnchorResult::Type AnchorResult, const TArray<FOculusXRSpaceQueryResult>& QueryResults, FOculusXRUInt64 RoomSpaceID, FOculusXRRoomLayout RoomLayout)
{
	if (QueryResults.Num() != 1)
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("Wrong number of elements returned from query for floor UUID. Result count (%d), UUID (%s), Room Space ID (%llu)"), QueryResults.Num(), *RoomLayout.FloorUuid.ToString(), RoomSpaceID.Value);
		return;
	}

	const FOculusXRSpaceQueryResult& floorQueryResult = QueryResults[0];

	TArray<FString> semanticClassifications;
	GetSemanticClassifications(floorQueryResult.Space.Value, semanticClassifications);
	if (!semanticClassifications.Contains("FLOOR"))
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("Queried floor in room doesn't contain a floor semantic label. UUID (%s), Room Space ID (%llu)"), *RoomLayout.FloorUuid.ToString(), RoomSpaceID.Value);
		return;
	}

	EOculusXRAnchorResult::Type getBoundaryResult;
	TArray<FVector2f> boundaryVertices;
	if (!OculusXRAnchors::FOculusXRAnchors::GetSpaceBoundary2D(floorQueryResult.Space.Value, boundaryVertices, getBoundaryResult))
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("Failed to get space boundary vertices for floor. UUID (%s), Room Space ID (%llu)"), *RoomLayout.FloorUuid.ToString(), RoomSpaceID.Value);
		return;
	}

	OculusXRHMD::FOculusXRHMD* HMD = OculusXRHMD::FOculusXRHMD::GetOculusXRHMD();
	check(HMD);

	OculusXRHMD::FPose headPose;
	HMD->GetCurrentPose(OculusXRHMD::ToExternalDeviceId(ovrpNode_Head), headPose.Orientation, headPose.Position);

	TArray<FVector> convertedBoundaryPoints;

	FTransform floorTransform;
	if (!UOculusXRAnchorBPFunctionLibrary::GetAnchorTransformByHandle(floorQueryResult.Space, floorTransform))
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("Failed to get the floor anchor transform. Floor Space ID (%llu)"), floorQueryResult.Space.Value);
		return;
	}

	// Convert the boundary vertices to game engine world space
	for (auto& it : boundaryVertices)
	{
		FVector pos = floorTransform.TransformPosition(FVector(0, it.X * HMD->GetWorldToMetersScale(), it.Y * HMD->GetWorldToMetersScale()));
		convertedBoundaryPoints.Add(pos);
	}

	// Create the new 2D boundary
	TArray<FVector2f> new2DBoundary;
	for (auto& it : convertedBoundaryPoints)
	{
		new2DBoundary.Add(FVector2f(it.X, it.Y));
	}

	// Check if inside poly
	if (!PointInPolygon2D(FVector2f(headPose.Position.X, headPose.Position.Y), new2DBoundary))
	{
		UE_LOG(LogOculusXRScene, Verbose, TEXT("Floor failed active room check. UUID (%s), Room Space ID (%llu)"), *RoomLayout.FloorUuid.ToString(), RoomSpaceID.Value);
		return;
	}

	StartSingleRoomQuery(RoomSpaceID, RoomLayout);
}

bool AOculusXRSceneActor::PointInPolygon2D(FVector2f PointToTest, const TArray<FVector2f>& PolyVerts) const
{
	if (PolyVerts.Num() < 3)
	{
		return false;
	}

	int collision = 0;
	float x = PointToTest.X;
	float y = PointToTest.Y;

	int vertCount = PolyVerts.Num();
	for (int i = 0; i < vertCount; i++)
	{
		float x1 = PolyVerts[i].X;
		float y1 = PolyVerts[i].Y;

		float x2 = PolyVerts[(i + 1) % vertCount].X;
		float y2 = PolyVerts[(i + 1) % vertCount].Y;

		if (y < y1 != y < y2 && x < x1 + ((y - y1) / (y2 - y1)) * (x2 - x1))
		{
			collision += (y1 < y2) ? 1 : -1;
		}
	}

	return collision != 0;
}

void AOculusXRSceneActor::GetSemanticClassifications(uint64 Space, TArray<FString>& OutSemanticLabels) const
{
	EOculusXRAnchorResult::Type SemanticLabelAnchorResult;
	bool Result = OculusXRAnchors::FOculusXRAnchors::GetSpaceSemanticClassification(Space, OutSemanticLabels, SemanticLabelAnchorResult);
	if (Result)
	{
		UE_LOG(LogOculusXRScene, Verbose, TEXT("GetSemanticClassifications -- Space (%llu) Classifications:"), Space);
		for (FString& label : OutSemanticLabels)
		{
			UE_LOG(LogOculusXRScene, Verbose, TEXT("%s"), *label);
		}
	}
	else
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("SpatialAnchorQueryResult_Handler Failed to get semantic classification space."));
	}
}

// DELEGATE HANDLERS
void AOculusXRSceneActor::SceneCaptureComplete_Handler(FOculusXRUInt64 RequestId, bool bResult)
{
	if (!bResult)
	{
		UE_LOG(LogOculusXRScene, Error, TEXT("Scene Capture Complete failed!"));
		return;
	}

	// Mark that we already launched Capture Flow and try to query spatial anchors again
	bCaptureFlowWasLaunched = true;

	ClearScene();
	PopulateScene();
}

void AOculusXRSceneActor::PostLoad()
{
	Super::PostLoad();

	FOculusXRSpawnedSceneAnchorProperties desk;
	if (ScenePlaneSpawnedSceneAnchorProperties.RemoveAndCopyValue(TEXT("DESK"), desk))
	{
		UE_LOG(LogOculusXRScene, Log, TEXT("Running XR Scene Actor plane semantic lable migration: 'DESK' to 'TABLE'"));
		ScenePlaneSpawnedSceneAnchorProperties[TEXT("TABLE")] = desk;
	}

	if (SceneVolumeSpawnedSceneAnchorProperties.RemoveAndCopyValue(TEXT("DESK"), desk))
	{
		UE_LOG(LogOculusXRScene, Log, TEXT("Running XR Scene Actor volume semantic lable migration: 'DESK' to 'TABLE'"));
		SceneVolumeSpawnedSceneAnchorProperties[TEXT("TABLE")] = desk;
	}
}

#undef LOCTEXT_NAMESPACE
