// Copyright (c) Meta Platforms, Inc. and affiliates.

#pragma once

#include "Dom/JsonObject.h"
#include "GameFramework/Actor.h"
#include "GameFramework/WorldSettings.h"
#include "MRUtilityKitRoom.h"
#include "MRUtilityKit.h"
#include "MRUtilityKitData.h"
#include "OculusXRAnchorsRequests.h"
#include "Subsystems/GameInstanceSubsystem.h"
#include "Tickable.h"

#include "MRUtilityKitSubsystem.generated.h"

/**
 * The Mixed Reality Utility Kit subsystem.
 *
 * This subsystem acts as a container for scene/anchor data. It has methods to load
 * the scene data from the device or a JSON file. After the scene data has been loaded
 * it will be stored inside the subsystem to make it possible to query the data from
 * everywhere. In addition, it offers methods to fulfill queries on the scene data
 * like ray casts or simple content placement.
 *
 * The subsystem only contains core functionality that is useful for most cases.
 * More specific functionality is part of actors. For example, if your goal is to spawn
 * meshes in the place of scene anchors you can place the AMRUKAnchorActorSpawner in the
 * level to do this. When a level loads you would first load the anchor data from the
 * device with this subsystem by calling LoadSceneFromDevice() and then the AMRUKAnchorActorSpawner
 * will listen for the subsystem to load the scene data and then spawn the actors accordingly.
 *
 * You can expect methods in this subsystem to take all loaded rooms into consideration when computing.
 * If you want to use a method only on a single specific room, there is most of the time a method
 * with the same name on the AMRUKRoom.
 */
UCLASS(ClassGroup = MRUtilityKit)
class MRUTILITYKIT_API UMRUKSubsystem : public UGameInstanceSubsystem, public FTickableGameObject
{
	GENERATED_BODY()

public:
	DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLoaded, bool, Success);
	DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCaptureComplete, bool, Success);
	DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRoomCreated, AMRUKRoom*, Room);
	DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRoomUpdated, AMRUKRoom*, Room);
	DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRoomRemoved, AMRUKRoom*, Room);

	/**
	 * The status of the scene loading. When loading from device this is an asynchronous process
	 * so will be in the Busy state until it moves to Complete or Failed.
	 */
	UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
	EMRUKInitStatus SceneLoadStatus = EMRUKInitStatus::None;

	/**
	 * An event that will trigger when a scene is loaded either from Device or from JSON.
	 * The Success parameter indicates whether the scene was loaded successfully or not.
	 */
	UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
	FOnLoaded OnSceneLoaded;

	/**
	 * An event that gets fired after a room has been created.
	 */
	UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
	FOnRoomCreated OnRoomCreated;

	/**
	 * An event that gets fired after a room has been updated.
	 */
	UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
	FOnRoomUpdated OnRoomUpdated;

	/**
	 * An event that gets fired when a room gets removed.
	 */
	UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
	FOnRoomRemoved OnRoomRemoved;

	/**
	 * An event that will trigger when the capture flow completed.
	 * The Success parameter indicates whether the scene was captured successfully or not.
	 */
	UPROPERTY(BlueprintAssignable, Category = "MR Utility Kit")
	FOnCaptureComplete OnCaptureComplete;

	/**
	 * Contains a list of rooms that are tracked by the mixed reality utility kit subsystem.
	 */
	UPROPERTY(VisibleInstanceOnly, Transient, BlueprintReadOnly, Category = "MR Utility Kit")
	TArray<TObjectPtr<AMRUKRoom>> Rooms;

	/**
	 * When world locking is enabled the position of the VR Pawn will be adjusted each frame to ensure
	 * the room anchors are where they should be relative to the camera position. This is necessary to
	 * ensure the position of the virtual objects in the world do not get out of sync with the real world.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "MR Utility Kit")
	bool EnableWorldLock = true;


	/**
	 * Cast a ray and return the closest hit anchor in the scene.
	 * @param Origin      Origin The origin of the ray.
	 * @param Direction   Direction The direction of the ray.
	 * @param MaxDist     The maximum distance the ray should travel.
	 * @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
	 * @param OutHit      The closest hit.
	 * @return            The anchor that the ray hit
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
	AMRUKAnchor* Raycast(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, FMRUKHit& OutHit);

	/**
	 * Cast a ray and collect hits against the volumes and plane bounds in every room in the scene.
	 * The order of the hits in the array is not specified.
	 * @param Origin      Origin The origin of the ray.
	 * @param Direction   Direction The direction of the ray.
	 * @param MaxDist     The maximum distance the ray should travel.
	 * @param LabelFilter The label filter can be used to include/exclude certain labels from the search.
	 * @param OutHits     The hits the ray collected.
	 * @param OutAnchors  The anchors that were hit. Each anchor in this array corresponds to a entry at the same position in OutHits.
	 * @return            Whether the ray hit anything
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
	bool RaycastAll(const FVector& Origin, const FVector& Direction, float MaxDist, const FMRUKLabelFilter& LabelFilter, TArray<FMRUKHit>& OutHits, TArray<AMRUKAnchor*>& OutAnchors);

	/**
	 * Return the room that the headset is currently in. If the headset is not in any given room
	 * then it will return the room the headset was last in when this function was called.
	 * If the headset hasn't been in a valid room yet then return the first room in the list.
	 * If no rooms have been loaded yet then return null.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	AMRUKRoom* GetCurrentRoom() const;

	/**
	 * Save all rooms and anchors to JSON. This JSON representation can than later be used by
	 * LoadSceneFromJsonString() to load the scene again.
	 * @return the JSON string.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	FString SaveSceneToJsonString();

	/**
	 * Load rooms and anchors from a JSON representation.
	 * If the scene is already loaded the scene will be updated with the changes.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	void LoadSceneFromJsonString(const FString& String);

	/**
	 * Load rooms and anchors from the device.
	 * If the scene is already loaded the scene will be updated with the changes.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	void LoadSceneFromDevice();


	/**
	 * Removes and clears every room.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	void ClearScene();

	/**
	 *  Get the position on the surface that is closest to the given position with respect to the distance in all rooms.
	 *  @param WorldPosition      The position in world space from which the closest surface point should be found.
	 *  @param OutSurfacePosition The closest position on the closest surface if any. Otherwise zero.
	 *  @param LabelFilter        The label filter can be used to include/exclude certain labels from the search.
	 *  @param MaxDistance        The distance to which a closest surface position should be searched. Everything below or equal to zero will be treated as infinity.
	 *  @return                   The Anchor on which the closest surface position was found or a null pointer otherwise.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
	AMRUKAnchor* TryGetClosestSurfacePosition(const FVector& WorldPosition, FVector& OutSurfacePosition, const FMRUKLabelFilter& LabelFilter, double MaxDistance = 0.0);

	/**
	 * Finds the closest seat given a ray.
	 * @param RayOrigin The origin of the ray.
	 * @param RayDirection The direction of the ray.
	 * @param OutSeatTransform The seat pose.
	 * @return If any seat was found the Anchor that has seats available will be returned. Otherwise a null pointer.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	AMRUKAnchor* TryGetClosestSeatPose(const FVector& RayOrigin, const FVector& RayDirection, FTransform& OutSeatTransform);

	/**
	 * Get a suggested pose (position & rotation) from a raycast to place objects on surfaces in the scene.
	 * There are different positioning modes available. Default just uses the position where the raycast
	 * hit the object. Edge snaps the position to the edge that is nearest to the user and Center simply
	 * centers the position on top of the surface.
	 * @param RayOrigin         The origin of the ray.
	 * @param RayDirection      The direction of the ray.
	 * @param MaxDist           The maximum distance the ray should travel.
	 * @param LabelFilter       The label filter can be used to include/exclude certain labels from the search.
	 * @param OutPose           The calculated pose.
	 * @param PositioningMethod The method that should be used for determining the position on the surface.
	 * @return                  The anchor that was hit by the ray if any. Otherwise a null pointer.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit", meta = (AutoCreateRefTerm = "LabelFilter"))
	AMRUKAnchor* GetBestPoseFromRaycast(const FVector& RayOrigin, const FVector& RayDirection, double MaxDist, const FMRUKLabelFilter& LabelFilter, FTransform& OutPose, EMRUKPositioningMethod PositioningMethod = EMRUKPositioningMethod::Default);

	/**
	 * Return the longest wall in the current room that has no other walls behind it.
	 * @param Tolerance The tolerance to use when determining wall that are behind.
	 * @return          The wall anchor that is the key wall in the room.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	AMRUKAnchor* GetKeyWall(double Tolerance = 0.1);

	/**
	 * Return the largest surface for a given label in the current room.
	 * @param Label The label of the surfaces to search in.
	 * @return      The anchor that has the largest surface if any. Otherwise, a null pointer.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	AMRUKAnchor* GetLargestSurface(const FString& Label);

	/**
	 * Checks if the given position is on or inside of any scene volume in the rooms.
	 * All rooms will be checked and the first anchors scene volume that has the point on or inside it will be returned.
	 * @param WorldPosition      The position in world space to check
	 * @param TestVerticalBounds Whether the vertical bounds should be checked or not
	 * @param Tolerance          Tolerance
	 * @return					 The anchor the WorldPosition is in. A null pointer otherwise.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	AMRUKAnchor* IsPositionInSceneVolume(const FVector& WorldPosition, bool TestVerticalBounds = true, double Tolerance = 0.0);

	/**
	 * Spawn meshes on the position of the anchors of each room.
	 * The actors should have Z as up Y as right and X as forward.
	 * The pivot point should be in the bottom center.
	 * @param SpawnGroups                A map which tells to spawn which actor to a given label.
	 * @param ProceduralMaterial         Material to apply on top of the procedural mesh if any.
	 * @param CutHoleLabels		         Labels for which the generated mesh should have holes. Only works with planes.
	 * @param ShouldFallbackToProcedural Whether or not it should by default fallback to generating a procedural mesh if no actor class has been specified for a label.
	 * @return                           The spawned actors.
	 */
	UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage = "Use AMRUKAnchorActorSpawner instead."), Category = "MR Utility Kit")
	TArray<AActor*> SpawnInterior(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial = nullptr, bool ShouldFallbackToProcedural = true);

	/**
	 * Spawn meshes on the position of the anchors of each room from a random stream.
	 * The actors should have Z as up Y as right and X as forward.
	 * The pivot point should be in the bottom center.
	 * @param SpawnGroups                A map which tells to spawn which actor to a given label.
	 * @param RandomStream               A random generator to choose randomly between actor classes if there a multiple for one label.
	 * @param CutHoleLabels		         Labels for which the generated mesh should have holes. Only works with planes.
	 * @param ProceduralMaterial         Material to apply on top of the procedural mesh if any.
	 * @param ShouldFallbackToProcedural Whether or not it should by default fallback to generating a procedural mesh if no actor class has been specified for a label.
	 * @return                           The spawned actors.
	 */
	UFUNCTION(BlueprintCallable, meta = (DeprecatedFunction, DeprecationMessage = "Use AMRUKAnchorActorSpawner instead."), Category = "MR Utility Kit")
	TArray<AActor*> SpawnInteriorFromStream(const TMap<FString, FMRUKSpawnGroup>& SpawnGroups, const FRandomStream& RandomStream, const TArray<FString>& CutHoleLabels, UMaterialInterface* ProceduralMaterial = nullptr, bool ShouldFallbackToProcedural = true);

	/**
	 * Launch the scene capture. After a successful capture the scene should be updated.
	 * @return Whether the capture was successful.
	 */
	UFUNCTION(BlueprintCallable, Category = "MR Utility Kit")
	bool LaunchSceneCapture();

public:
	void Initialize(FSubsystemCollectionBase& Collection) override;
	void Deinitialize() override;

	TSharedRef<FJsonObject> JsonSerialize();
	void UnregisterRoom(AMRUKRoom* Room);
	// Calculate the bounds of an Actor class and return it, the result is saved in a cache for faster lookup.
	FBox GetActorClassBounds(TSubclassOf<AActor> Actor);
	UOculusXRRoomLayoutManagerComponent* GetRoomLayoutManager();

private:
	AMRUKRoom* SpawnRoom();

	void FinishedLoading(bool Success);

	// FTickableGameObject interface
	virtual void Tick(float DeltaTime) override;
	virtual bool IsTickable() const override;
	virtual ETickableTickType GetTickableTickType() const override { return (HasAnyFlags(RF_ClassDefaultObject) ? ETickableTickType::Never : ETickableTickType::Conditional); }
	virtual TStatId GetStatId() const override { RETURN_QUICK_DECLARE_CYCLE_STAT(UMRUKSubsystem, STATGROUP_Tickables); }
	virtual UWorld* GetTickableGameObjectWorld() const override { return GetWorld(); }
	// ~FTickableGameObject interface

	UFUNCTION()
	void SceneDataLoadedComplete(bool Success);
	UFUNCTION()
	void UpdatedSceneDataLoadedComplete(bool Success);
	UFUNCTION()
	void SceneCaptureComplete(FOculusXRUInt64 RequestId, bool bSuccess);

	UPROPERTY()
	TObjectPtr<UMRUKSceneData> SceneData = nullptr;

	UPROPERTY()
	AActor* RoomLayoutManagerActor = nullptr;
	UPROPERTY()
	UOculusXRRoomLayoutManagerComponent* RoomLayoutManager = nullptr;
	UPROPERTY()
	mutable AMRUKRoom* CachedCurrentRoom = nullptr;
	mutable int64 CachedCurrentRoomFrame = 0;
	UPROPERTY()
	AActor* PositionGenerator = nullptr;

	TMap<TSubclassOf<AActor>, FBox> ActorClassBoundsCache;
};