// Copyright (c) Microsoft Corporation. 
// Licensed under the MIT license.

#include "Tests/AutomationCommon.h"
#include "Tests/AutomationEditorCommon.h"

#include "Engine/World.h"
#include "Engine/LODActor.h"
#include "EngineUtils.h"
#include "HierarchicalLOD.h"
#include "LODUtilities.h"

#include "SimplygonTestsHelper.h"
#include "LODRecipe.h"	
#include "LODRecipeManager.h"
#include "StandinActor.h"
#include "StandinUtilitiesModule.h"

DEFINE_LOG_CATEGORY_STATIC(LogSimplygonSimpleTests, All, All);

/* Simple Plugin Tests*/
BEGIN_DEFINE_SPEC(FSimplygonSimple, "Simplygon.Simple", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
	void RunSimpleStandinTest(
		const FString& AssetPath, 
		const int32 ExpectedNumTriangles, 
		const int32 ExpectedNumMaterials, 
		const float Tolerance = KINDA_SMALL_NUMBER);
END_DEFINE_SPEC(FSimplygonSimple)

void FSimplygonSimple::Define()
{
	It("Make sure LOD Recipe generates LODs using default values", [this]()
	{
		FName PathToAsset = TEXT("/Game/Content/StaticMeshes/Donut/donut_Donut.donut_Donut");
		UStaticMesh* StaticMesh = SimplygonTestsHelper::GetAssetByPath<UStaticMesh>(PathToAsset);
		if (!StaticMesh)
		{
			UE_LOG(LogSimplygonSimpleTests, Error, TEXT("Failed to find Static Mesh: %s"), *PathToAsset.ToString());
			return;
		}

		// Always recache the static mesh
		StaticMesh->BuildCacheAutomationTestGuid = FGuid::NewGuid();

		int32 OriginalNumTriangles = StaticMesh->GetRenderData()->LODResources[0].GetNumTriangles();

		// Expected values
		const int32 ExpectedNumLODs = 4;
		const TArray<float> ExpectedReductionValues = { 1.0f, 0.5f, 0.25f, 0.125f };
		const TArray<int32> ExpectedNumTriangles = 
		{ 
			OriginalNumTriangles,
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[1]), 
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[2]),
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[3]),
		};

		// Create LOD Recipe dynamically
		ULODRecipe* LODRecipe = NewObject<ULODRecipe>(GetTransientPackage(), NAME_None, RF_NoFlags);
		LODRecipe->AddMesh(StaticMesh);

		// Check LOD Recipe reduction values
		const TArray<USimplygonPipeline*>& Pipelines = LODRecipe->GetPipelines();
		int32 NumValidPipelines = 0;
		for (int32 LODIndex = 1; LODIndex < Pipelines.Num(); ++LODIndex)
		{
			UReductionPipeline* ReductionPipeline = Cast<UReductionPipeline>(Pipelines[LODIndex]);
			if (ReductionPipeline != nullptr)
			{
				FString LODText = "LOD" + FString::FormatAsNumber(LODIndex);
				TestEqual(LODText + " - Use triangle ratio", ReductionPipeline->Settings.ReductionSettings.ReductionTargetTriangleRatioEnabled, true);
				TestEqual(LODText + " - Use triangle count", ReductionPipeline->Settings.ReductionSettings.ReductionTargetTriangleCountEnabled, false);
				TestEqual(LODText + " - Use max deviation", ReductionPipeline->Settings.ReductionSettings.ReductionTargetMaxDeviationEnabled, false);
				TestEqual(LODText + " - Use on screen size", ReductionPipeline->Settings.ReductionSettings.ReductionTargetOnScreenSizeEnabled , false);

				const float& ActualReductionRatio = ReductionPipeline->Settings.ReductionSettings.ReductionTargetTriangleRatio;
				TestEqual(LODText + " traingle ratio", ActualReductionRatio, ExpectedReductionValues[LODIndex]);

				NumValidPipelines++;
			}
		}

		// Check if we have the correct number of pipelines enabled in LOD Recipe
		// NOTE: We are not allowing LOD0 optimizations, hence "ExpectedNumLODs - 1"
		TestEqual("Number of enabled pipelines in LOD Recipe", NumValidPipelines, ExpectedNumLODs - 1 );

		// Generate LODs
		FLODRecipeManager::Get().Build(TArray<ULODRecipe*>{ LODRecipe });

		// Check number of LODs and LOD triangle count
		TestEqual("Number of LODs", StaticMesh->GetNumLODs(), ExpectedNumLODs);
			
		for (int32 LODIndex = 1; LODIndex < StaticMesh->GetNumLODs(); ++LODIndex)
		{
			int32 ActualNumTriangles = StaticMesh->GetRenderData()->LODResources[LODIndex].GetNumTriangles();
			TestEqual("Number of triangles in LOD" + FString::FormatAsNumber(LODIndex), ActualNumTriangles, ExpectedNumTriangles[LODIndex], 5.0f);
		}
	});

	It("Build one HLOD cluster", [this]()
	{
		FAutomationEditorCommonUtils::LoadMap("/Game/Maps/Simple_HLOD");
		UWorld* CurrentWorld = SimplygonTestsHelper::GetCurrentWorld();
		if (!CurrentWorld)
		{
			UE_LOG(LogSimplygonSimpleTests, Error, TEXT("Failed to find world."));
			return;
		}

		if (!CurrentWorld->HierarchicalLODBuilder)
		{
			UE_LOG(LogSimplygonSimpleTests, Error, TEXT("Failed to find HLOD Builder."));
			return;
		}

		CurrentWorld->HierarchicalLODBuilder->BuildMeshesForLODActors(true);
		if (!SimplygonTestsHelper::WaitForHLODProcessor(30.0))
		{
			UE_LOG(LogSimplygonSimpleTests, Error, TEXT("Timeout while waiting for HLOD Proxy Processor to complete."));
			return;
		}
		
		// Find LOD Actor and store number of triangles
		int32 ActualNumLODActors = 0;
		int32 ActualNumTriangles = 0;
		for (TActorIterator<ALODActor> ItActor = TActorIterator<ALODActor>(CurrentWorld); ItActor; ++ItActor)
		{
			ALODActor* LODActor = *ItActor;
			UStaticMesh* ProxyMesh = ItActor->GetStaticMeshComponent()->GetStaticMesh();

			if (ProxyMesh != nullptr)
			{
				ActualNumTriangles = ProxyMesh->GetRenderData()->LODResources[0].GetNumTriangles();
				ActualNumLODActors++;
			}
		}

		TestEqual("Number of LOD Actors", ActualNumLODActors, 1);

		// At time of writing, the generated mesh got 552 triangels
		int32 ExpectedNumTriangles = 552;
		TestEqual("Proxy mesh number of triangles", (float)ActualNumTriangles, (float)ExpectedNumTriangles, 100.0f);
	});

	It("Apply LODGroup to Static Mesh", [this]()
	{
		FName PathToAsset = TEXT("/Game/Content/StaticMeshes/Donut/donut_Icing.donut_Icing");
		UStaticMesh* StaticMesh = SimplygonTestsHelper::GetAssetByPath<UStaticMesh>(PathToAsset);
		if (!StaticMesh)
		{
			UE_LOG(LogSimplygonSimpleTests, Error, TEXT("Failed to find Static Mesh: %s"), *PathToAsset.ToString());
			return;
		}

		// Always recache the static mesh
		StaticMesh->BuildCacheAutomationTestGuid = FGuid::NewGuid();

		int32 OriginalNumTriangles = StaticMesh->GetRenderData()->LODResources[0].GetNumTriangles();
		
		// Expected values
		const int32 ExpectedNumLODs = 4;
		const TArray<float> ExpectedReductionValues = { 1.0f, 0.5f, 0.25f, 0.125f };
		const TArray<int32> ExpectedNumTriangles =
		{
			OriginalNumTriangles,
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[1]),
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[2]),
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[3]),
		};

		// Apply LODGroup
		StaticMesh->SetLODGroup(TEXT("SmallProp"));
		StaticMesh->Build();
		StaticMesh->PostEditChange();

		// Check number of LODs and LOD triangle count
		TestEqual("Number of LODs", StaticMesh->GetNumLODs(), ExpectedNumLODs);

		for (int32 LODIndex = 1; LODIndex < StaticMesh->GetNumLODs(); ++LODIndex)
		{
			int32 ActualNumTriangles = StaticMesh->GetRenderData()->LODResources[LODIndex].GetNumTriangles();
			TestEqual("Number of triangles in LOD" + FString::FormatAsNumber(LODIndex), ActualNumTriangles, ExpectedNumTriangles[LODIndex], 5.0f);
		}
	});

	It("Generate skeletal mesh LODs", [this]()
	{
		FName PathToAsset = TEXT("/Game/Content/SkeletalMeshes/Cylinder_w_morphtargets/cylinder_wo_targets.cylinder_wo_targets");
		USkeletalMesh* SkeletalMesh = SimplygonTestsHelper::GetAssetByPath<USkeletalMesh>(PathToAsset);
		if (!SkeletalMesh)
		{
			UE_LOG(LogSimplygonSimpleTests, Error, TEXT("Failed to find skeletal mesh: %s"), *PathToAsset.ToString());
			return;
		}

		FSkeletalMeshModel* SkeletalMeshResource = SkeletalMesh->GetImportedModel();
		int32 OriginalNumTriangles = SimplygonTestsHelper::GetNumTrianglesInLODModel(&SkeletalMeshResource->LODModels[0]);

		// Expected values
		const int32 ExpectedNumLODs = 4;
		// NOTE: There is a engine bug producing wrong(?) default reduction values by skipping 0.5.
		const TArray<float> ExpectedReductionValues = { 1.0f, 0.25f, 0.125f, 0.06f };
		const TArray<int32> ExpectedNumTriangles =
		{
			OriginalNumTriangles,
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[1]),
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[2]),
			FMath::RoundToInt(OriginalNumTriangles * ExpectedReductionValues[3]),
		};

		FLODUtilities::RegenerateLOD(SkeletalMesh, GetTargetPlatformManagerRef().GetRunningTargetPlatform(), 4, false, false);

		int32 ActualNumLODs = SkeletalMeshResource->LODModels.Num();
		TestEqual("Number of LODs", ActualNumLODs, ExpectedNumLODs);

		for (int32 LODIndex = 1; LODIndex < SkeletalMesh->GetLODNum(); ++LODIndex)
		{
			int32 ActualNumTriangles = SimplygonTestsHelper::GetNumTrianglesInLODModel(&SkeletalMeshResource->LODModels[LODIndex]);
			TestEqual("Number of triangles in LOD" + FString::FormatAsNumber(LODIndex), ActualNumTriangles, ExpectedNumTriangles[LODIndex], 5.0f);
		}
	});

	It("Generate Far Standin using default settings", [this]()
	{
		RunSimpleStandinTest("/Game/Maps/Simple_Standin_Far", 694, 1, 50.0f);
	});

	It("Generate Near Standin using default settings", [this]()
	{
		RunSimpleStandinTest("/Game/Maps/Simple_Standin_Near", 2480, 3, 2.0f);
	});
}

void FSimplygonSimple::RunSimpleStandinTest(const FString& AssetPath, const int32 ExpectedNumTriangles, const int32 ExpectedNumMaterials, const float Tolerance)
{
	FAutomationEditorCommonUtils::LoadMap(AssetPath);
	UWorld* CurrentWorld = SimplygonTestsHelper::GetCurrentWorld();
	if (!CurrentWorld)
	{
		UE_LOG(LogSimplygonSimpleTests, Error, TEXT("Failed to find world."));
		return;
	}

	IStandinUtilitiesModule& StandinModule = FModuleManager::LoadModuleChecked<IStandinUtilitiesModule>("StandinUtilities");

	TArray<AStandInMeshActor*> StandinActors;
	StandinModule.GetStandinActors(StandinActors);
	AStandInMeshActor* StandinActor = StandinActors.Last();

	int32 OriginalNumTriangles = StandinActor->GetNumChildrenTriangles();

	StandinModule.GenerateStandinMeshes(StandinActors);
		
	UStaticMesh* StaticMesh = StandinActor->GetStaticMesh();
	int32 ActualNumTriangles = StaticMesh->GetRenderData()->LODResources[0].GetNumTriangles();
	TestEqual("Number of triangles", (float)ActualNumTriangles, (float)ExpectedNumTriangles, Tolerance);

	const TArray<FStaticMaterial>& Materials = StaticMesh->GetStaticMaterials();
	const int32 ActualNumMaterials = Materials.Num();
	TestEqual("Number of materials", ActualNumMaterials, ExpectedNumMaterials);
}