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

#include "SimplygonReduction.h"
#include "SimplygonDataConverter.h"
#if WITH_EDITOR
#if PLATFORM_WINDOWS
THIRD_PARTY_INCLUDES_START
#include "Windows\AllowWindowsPlatformTypes.h"
#include "Simplygon.h"
#include "Windows\HideWindowsPlatformTypes.h"
THIRD_PARTY_INCLUDES_END
#endif // PLATFORM_WINDOWS
#endif
#include "SimplygonModule.h"
#include "ScopedFPExceptionMaskState.h"
#include "SimplygonContent/Classes/SimplygonSettings.h"
#include "DefaultEventHandler.h"
#include "SimplygonUE4UI.h"

#include "MaterialUtilities.h"
#include "MeshAttributes.h"
#include "StaticMeshAttributes.h"
#include "MeshBoneReduction.h"
#include "MeshDescription.h"
#include "MeshMergeData.h"
#include "MeshUtilities.h"
#include "Engine/MeshMerging.h"
#include "IMeshBuilderModule.h"

#include "AnimationBlueprintLibrary.h"

#include "HAL/RunnableThread.h"
#include "Misc/ScopedSlowTask.h"
#include "ProfilingDebugging/ScopedTimers.h"
#include "Rendering/SkeletalMeshModel.h"
#include "ClothingAsset.h"

#include "ComponentReregisterContext.h"
#include "AnimationRuntime.h"
#include "Telemetry.h"
#include "TelemetryManager.h"
#include "HAL/RunnableThread.h"

#define LOCTEXT_NAMESPACE "SimplygonPluginMeshReduction"
DEFINE_LOG_CATEGORY_STATIC(LogSimplygon, VeryVerbose, All);

class FSimplygonReduceStaticMeshRunnable : public FSimplygonRunnable
{
public:
	FSimplygonReduceStaticMeshRunnable(FSimplygonReduction* InReductionModule,
		const FSimplygonJobData& InJobData,
		Simplygon::spScene InScene,
		Simplygon::spPipeline InPipeline,
		const FSimplygonProcessProgressDelegate& InJobProgressDelegate,
		const FSimplygonProcessCompleteDelegate& InJobCompleteDelegate,
		const FSimplygonProcessFailedDelegate& InJobFailedDelegate)
		: ReductionModule(InReductionModule)
		, JobData(InJobData)
		, Scene(InScene)
		, Pipeline(InPipeline)
		, JobProgressDelegate(InJobProgressDelegate)
		, JobCompleteDelegate(InJobCompleteDelegate)
		, JobFailedDelegate(InJobFailedDelegate)
	{
	}

	virtual uint32 Run()
	{
		ReductionModule->RunSceneSync(JobData, Scene, Pipeline, JobProgressDelegate, JobCompleteDelegate, JobFailedDelegate);
		IsDone = true;
		return 0;
	}

private:
	FSimplygonReduction* ReductionModule;
	FSimplygonJobData JobData;
	Simplygon::spScene Scene;
	Simplygon::spPipeline Pipeline;
	FSimplygonProcessProgressDelegate JobProgressDelegate;
	FSimplygonProcessCompleteDelegate JobCompleteDelegate;
	FSimplygonProcessFailedDelegate JobFailedDelegate;
};

class FSimplygonPipelineBatchRunnable : public FSimplygonRunnable
{
public:
	FSimplygonPipelineBatchRunnable(FSimplygonReduction* InReductionModule,
		const FSimplygonJobData& InJobData,
		Simplygon::spPipelineBatch InPipelineBatch,
		const FSimplygonProcessProgressDelegate& InJobProgressDelegate,
		const FSimplygonProcessBatchCompleteDelegate& InJobCompleteDelegate,
		const FSimplygonProcessFailedDelegate& InJobFailedDelegate)
		: ReductionModule(InReductionModule)
		, JobData(InJobData)
		, Batch(InPipelineBatch)
		, JobProgressDelegate(InJobProgressDelegate)
		, JobCompleteDelegate(InJobCompleteDelegate)
		, JobFailedDelegate(InJobFailedDelegate)
	{
	}

	virtual uint32 Run()
	{
		ReductionModule->RunBatchSync(JobData, Batch, JobProgressDelegate, JobCompleteDelegate, JobFailedDelegate);
		IsDone = true;
		return 0;
	}

private:
	FSimplygonReduction* ReductionModule;
	FSimplygonJobData JobData;
	Simplygon::spPipelineBatch Batch;
	FSimplygonProcessProgressDelegate JobProgressDelegate;
	FSimplygonProcessBatchCompleteDelegate JobCompleteDelegate;
	FSimplygonProcessFailedDelegate JobFailedDelegate;
};

static bool HasValidResult(Simplygon::spPipeline InPipeline)
{
	auto processedScene = InPipeline->GetProcessedScene();
	auto setId = processedScene->SelectNodes("ISceneMesh");
	if (setId < 0)
		return false;

	auto selectedMeshNodes = processedScene->GetSelectionSetTable()->GetSelectionSet(setId);
	if (selectedMeshNodes.IsNull())
		return false;

	//check there is actual vertices. 
	int OutTotalVertices = 0;
	for (auto i = 0U; i < selectedMeshNodes->GetItemCount(); ++i)
	{
		auto nodeId = selectedMeshNodes->GetItem(i);
		auto sceneMesh = Simplygon::spSceneMesh::SafeCast(processedScene->GetNodeByGUID(nodeId));
		if (!sceneMesh.IsNull())
		{
			OutTotalVertices += sceneMesh->GetGeometry()->GetCoords()->GetItemCount();
		}
	}

	if (OutTotalVertices == 0)
		return false;

	return true;
}

static bool
WasPipelineSuccessful( Simplygon::spPipeline Pipeline )
{
	if( Pipeline->GetProcessedScene().IsNull() )
		return Pipeline->GetPipelineSettings()->GetIntermediateStep();

	if (Pipeline->GetCascadedPipelineCount() == 0)
		return HasValidResult(Pipeline);

	//is cascaded pipeline
	for( unsigned int ipipeline = 0; ipipeline < Pipeline->GetCascadedPipelineCount(); ++ipipeline )
	{
		if( !WasPipelineSuccessful( Simplygon::spPipeline::SafeCast( Pipeline->GetCascadedPipelineByIndex( ipipeline ) ) ) )
			return false;
	}

	return true;
}

static bool
WasPipelineBatchSuccessful( Simplygon::spPipelineBatch Batch )
{
	for( unsigned int ipipeline = 0; ipipeline < Batch->GetBatchCount(); ++ipipeline )
	{
		if( Batch->GetOutputPath( ipipeline ).IsNullOrEmpty() )
			return false;
	}

	return true;
}

FSimplygonReduction::FSimplygonReduction()
{
	VersionString = TEXT("Simplygon");

	VisibilitySamplesHandler.BindLambda([&](const TArray<FVector>& InSamples)
	{
			VisibilitySamplePoints.Empty();
			for (const auto& Sample : InSamples)
				VisibilitySamplePoints.Add(Sample);
	});

	CuttingPlanesHandler.BindLambda([&](const TArray<FPlane>& Planes)
	{
			CuttingPlanes.Empty();
			for (const auto& Plane : Planes)
				CuttingPlanes.Add(Plane);
	});
}

bool FSimplygonReduction::Tick(float DeltaTime)
{
	FScopeLock Lock(&StateLock);

	TArray<FSimplygonRunnable*> LocalRunnables = Runnables;
	Runnables.Empty();

	for (auto* Runnable : LocalRunnables)
	{
		if (Runnable->IsDone)
		{
			Runnable->Thread->WaitForCompletion();
			delete Runnable->Thread;
			delete Runnable;
        }
		else
		{
			Runnables.Add(Runnable);
        }
	}

	return true;
}


//////////////////////////////////////////////////////////////////////////
// IMeshReduction

void FSimplygonReduction::ReduceMeshDescription(FMeshDescription& OutReducedMesh, float& OutMaxDeviation,
	const FMeshDescription& InMesh,
	const FOverlappingCorners& InOverlappingCorners,
	const struct FMeshReductionSettings& MeshReductionSettings)
{
	FTelemetry::Record(TEXT("ReduceMeshDescription"), SGTC::StaticMeshEditor);

	Simplygon::ISimplygon* SDK = ISimplygonModule::Get().GetSDK(true);
	if (SDK == nullptr)
		return;

	int32 LastWorkProgress = 0;
	FScopedSlowTask SlowTask(100, (LOCTEXT("SimplygonReduction_ReduceMeshDescription", "Simplygon generating LOD")));
	SlowTask.MakeDialog();

	// Create a Simplygon Scene
	Simplygon::spScene Scene = SDK->CreateScene();

	// Material maps
	TMap<FName, int32> MaterialNameToIndex;
	TMap<int32, FName> MaterialIndexToName;
	FSimplygonDataConverter::GetMaterialMaps(SDK, InMesh, MaterialNameToIndex, MaterialIndexToName);

	TArray<FBox2D> TexCoordBounds;
	TArray<FVector2D> NewUVs;

	Simplygon::spGeometryData GeometryData =
		FSimplygonDataConverter::CreateGeometryFromMeshDescription(SDK, InMesh, MaterialNameToIndex, TexCoordBounds, NewUVs, false);
	if (!GeometryData)
	{
		UE_LOG(LogSimplygon, Error, TEXT("Geometry data is NULL"));
		return;
	}
	GeometryData->CleanupNanValues();

	// Validate the geometry
	Simplygon::spGeometryValidator GeometryValidator = SDK->CreateGeometryValidator();
	ValidateGeometry(SDK, GeometryValidator, GeometryData);

	Simplygon::spSceneMesh Mesh = SDK->CreateSceneMesh();
	Mesh->SetGeometry(GeometryData);
	Mesh->SetName(TCHAR_TO_ANSI(*FString::Printf(TEXT("UnrealMesh"))));
	Scene->GetRootNode()->AddChild(Mesh);

	Simplygon::spReductionPipeline ReductionPipeline = SDK->CreateReductionPipeline();

	Simplygon::spRepairSettings RepairSettings = ReductionPipeline->GetRepairSettings();
	RepairSettings->SetWeldDist(MeshReductionSettings.WeldingThreshold);
	RepairSettings->SetTJuncDist(MeshReductionSettings.WeldingThreshold);

	Simplygon::spReductionSettings ReductionSettings = ReductionPipeline->GetReductionSettings();
	SetReductionSettings(ReductionSettings, MeshReductionSettings, GeometryData->GetTriangleCount());

	// Set visibility settings
	Simplygon::spVisibilitySettings VisibilitySettings = ReductionPipeline->GetVisibilitySettings();
	VisibilitySettings->SetCullOccludedGeometry(MeshReductionSettings.bCullOccluded);
	// +1 because there is an offset in aggressiveness options
	int32 TempAggressiveness = MeshReductionSettings.VisibilityAggressiveness + 1;
	VisibilitySettings->SetVisibilityWeightsPower(TempAggressiveness);
	VisibilitySettings->SetUseVisibilityWeightsInReducer(MeshReductionSettings.bVisibilityAided);

	Simplygon::spNormalCalculationSettings NormalSettings =
		ReductionPipeline->GetNormalCalculationSettings();
	SetNormalSettings(NormalSettings, MeshReductionSettings);

	FSimplygonProcessProgressDelegate JobProgressDelegate;
	JobProgressDelegate.BindLambda([&](const FSimplygonJobData& InJobData, int32 ProgressPercentage, int32 ProgressPercentageDelta)
		{
			if (ProgressPercentage > LastWorkProgress)
			{
				int32 Delta = ProgressPercentage - LastWorkProgress;
				SlowTask.EnterProgressFrame((float)Delta);
				LastWorkProgress = ProgressPercentage;
			}
		});

	RunPipeline(SDK, Simplygon::spPipeline::SafeCast(ReductionPipeline), Scene, false, FSimplygonJobData(), JobProgressDelegate);

	if (!WasPipelineSuccessful(Simplygon::spPipeline::SafeCast(ReductionPipeline)))
	{
		// Processing failed
		UE_LOG(LogSimplygon, Error, TEXT("Pipeline execution failed"));
		return;
	}

	Simplygon::spSceneMesh ReducedMesh =
		Simplygon::spSceneMesh::SafeCast(Scene->GetRootNode()->GetChild(0));
	FSimplygonDataConverter::CreateMeshDescriptionFromGeometry(ReducedMesh->GetGeometry(), MaterialIndexToName, OutReducedMesh, false);

	if (OutReducedMesh.IsEmpty())
	{
		// Mesh data conversion failed
		UE_LOG(LogSimplygon, Error, TEXT("Converting LOD data failed"));
		return;
	}

	OutMaxDeviation = ReductionPipeline->GetResultDeviation();
}

bool FSimplygonReduction::ReduceSkeletalMesh(class USkeletalMesh* SkeletalMesh, int32 LODIndex, const ITargetPlatform* TargetPlatform)
{
	FTelemetry::Record(TEXT("ReduceMeshDescription"), SGTC::Persona);

	UE_LOG(LogSimplygon, Log, TEXT("Begin ReduceSkeletalMesh"));

	Simplygon::ISimplygon* SDK = ISimplygonModule::Get().GetSDK(true);
	if (SDK == nullptr)
		return false;

	FSkeletalMeshModel* SkeletalMeshResource = SkeletalMesh->GetImportedModel();
	check(SkeletalMeshResource);
	check(LODIndex <= SkeletalMeshResource->LODModels.Num());

	ReduceSkeletalMeshWithResource(SDK, SkeletalMesh, SkeletalMeshResource, LODIndex);

	return true;
}

const FString& FSimplygonReduction::GetVersionString() const
{
	return VersionString;
}

bool FSimplygonReduction::IsSupported() const
{
	return true;
}

bool FSimplygonReduction::IsReductionActive(const struct FMeshReductionSettings& ReductionSettings) const
{
	bool bValidMaxDeviationValue = ReductionSettings.MaxDeviation > 0.0f;
	bool bValidTriangleRatio = ReductionSettings.PercentTriangles < 1.0f;
	bool bValidValues = bValidMaxDeviationValue || bValidTriangleRatio;
	return bValidValues;
}

bool FSimplygonReduction::IsReductionActive(
	const struct FSkeletalMeshOptimizationSettings& ReductionSettings) const
{
	return false;
}

bool FSimplygonReduction::IsReductionActive(const FSkeletalMeshOptimizationSettings& ReductionSettings, uint32 NumVertices, uint32 NumTriangles) const
{
	return false;
}

//////////////////////////////////////////////////////////////////////////
// IMeshMerging

void FSimplygonReduction::ProxyLOD(const TArray<FMeshMergeData>& InData, const FMeshProxySettings& InProxySettings, const TArray<FFlattenMaterial>& InputMaterials, const FGuid InJobGUID)
{
	FTelemetry::Record(TEXT("ProxyLOD"), SGTC::LevelEditor);

	if (!InData.Num())
	{
		FailedDelegate.ExecuteIfBound(
			InJobGUID, TEXT("The selected meshes are not valid. Make sure to select static meshes only."));
		return;
	}

	Simplygon::ISimplygon* SDK = ISimplygonModule::Get().GetSDK(true);
	if (SDK == nullptr)
		return;

	const USimplygonSettings* SimplygonSettings = GetDefault<USimplygonSettings>();
	FScopedSlowTask* SlowTask{};
	if( !SimplygonSettings->UseDistribution )
	{
		SlowTask = new FScopedSlowTask(100, (LOCTEXT("PluginUtils_GenerateLODS", "Simplygon generating proxy LOD")));
		SlowTask->MakeDialog();
	}

	// Create a Simplygon Scene
	Simplygon::spScene Scene = SDK->CreateScene();

	Simplygon::spGeometryValidator GeometryValidator = SDK->CreateGeometryValidator();
	TArray<FBox2D> GlobalTexcoordBounds;

	for (int32 MeshIndex = 0; MeshIndex < InData.Num(); ++MeshIndex)
		GlobalTexcoordBounds.Append(InData[MeshIndex].TexCoordBounds);

	// Clipping Geometry Set - Contains geometry that should be used for clipping.
	Simplygon::spSelectionSet ClippingGeometrySet = SDK->CreateSelectionSet();
	ClippingGeometrySet->SetName(SG_CLIPPING_GEOMETRY_SELECTION_SET_NAME);

	// Occluding Geometry Set - Contains geometry that should be treated as occluders.

	// Camera Set - Contains set of scene camera paths used for visibility.
	Simplygon::spSelectionSet CameraSet = SDK->CreateSelectionSet();
	CameraSet->SetName(SG_VISIBILITY_SELECTION_SET_NAME);

	// Clipping Planes Set - Contains set of scene planes for clipping.
	Simplygon::spSelectionSet ClippingPlaneSet = SDK->CreateSelectionSet();
	ClippingPlaneSet->SetName(SG_CLIPPING_PLANES_SELECTION_SET_NAME);

	FSimplygonJobData JobData;
	JobData.JobGuid = InJobGUID;
	int NumMeshToProcess = 0;
	TArray<Simplygon::spGeometryData> GeometryDataArray;
	GeometryDataArray.SetNum(InData.Num());
	// For each raw mesh in array create a scene mesh and populate with geometry data
	ParallelFor(InData.Num(), [&](int32 MeshIndex)
	{
		TMap<FName, int32> MaterialNameToIndex;
		TMap<int32, FName> IndexToMaterialName;
		FSimplygonDataConverter::GetMaterialMaps(SDK, *InData[MeshIndex].RawMesh, MaterialNameToIndex, IndexToMaterialName);

		Simplygon::spGeometryData GeometryData = 
			FSimplygonDataConverter::CreateGeometryFromMeshDescription(SDK, *InData[MeshIndex].RawMesh, MaterialNameToIndex, InData[MeshIndex].TexCoordBounds, InData[MeshIndex].NewUVs, false);
		if( !GeometryData )
		{
			UE_LOG(LogSimplygon, Error, TEXT("Geometry data is NULL"));
			FailedDelegate.ExecuteIfBound(InJobGUID, TEXT("Simplygon failed to generate Geometry Data"));
			return;
		}

		GeometryData->CleanupNanValues();
		GeometryDataArray[MeshIndex] = GeometryData;
	});
	for( int32 MeshIndex = 0; MeshIndex < InData.Num(); ++MeshIndex) 
	{
		Simplygon::spGeometryData GeometryData = GeometryDataArray[MeshIndex];

		// Validate the geometry
		ValidateGeometry(SDK, GeometryValidator, GeometryData);

		Simplygon::spSceneMesh Mesh = SDK->CreateSceneMesh();
		Mesh->SetGeometry(GeometryData);
		Mesh->SetName(TCHAR_TO_ANSI(*FString::Printf(TEXT("UnrealMesh%d"), MeshIndex)));
		Scene->GetRootNode()->AddChild(Mesh);

		// if mesh is clipping geometry add to clipping set else processing set
		if (InData[MeshIndex].bIsClippingMesh)
		{
			ClippingGeometrySet->AddItem(Mesh->GetNodeGUID());
		}
		else
		{
			NumMeshToProcess++;
		}
	}
	GeometryDataArray.Empty();

	if (NumMeshToProcess == 0)
	{
		delete SlowTask;
		FailedDelegate.ExecuteIfBound(
			InJobGUID, TEXT("No mesh to process. Can happen if mesh data provided to ProxyLOD method contained only meshes that are not suppose to be procesed."));
		return;
	}

	// add the sets to the scene
	//Scene->GetSelectionSetTable()->AddItem(ProcessingSet);
	Scene->GetSelectionSetTable()->AddItem(ClippingGeometrySet);

	// check if clipping planes are setup 
	bool bHasClippingPlanes = CuttingPlanes.Num() > 0;
	bool bHasClippingGeoemtry = ClippingGeometrySet->GetItemCount() > 0;

	//setup clipping planes scene in the scene.
	if (bHasClippingPlanes)
	{
		for (auto& Plane : CuttingPlanes)
		{
			Simplygon::spScenePlane TempPlane = SDK->CreateScenePlane();
			FVector Normal = Plane.GetSafeNormal();
			FVector Position(Plane.GetComponentForAxis(EAxis::X) + Plane.W, Plane.GetComponentForAxis(EAxis::Y) + Plane.W, Plane.GetComponentForAxis(EAxis::Z) + Plane.W);
			Normal = FSimplygonDataConverter::VectorToSimplygon(Normal);
			Position = FSimplygonDataConverter::VectorToSimplygon(Position);
			float Pos[3] = { Position.X,Position.Y,Position.Z };
			float Nor[3] = { Normal.X,Normal.Y,Normal.Z };

			TempPlane->SetPosition(&Pos[0]);
			TempPlane->SetNormal(&Nor[0]);

			Scene->GetRootNode()->AddChild(TempPlane);
			ClippingPlaneSet->AddItem(TempPlane->GetNodeGUID());
		}

		Scene->GetSelectionSetTable()->AddItem(ClippingPlaneSet);
	}

	Simplygon::spPipeline Pipeline;
	if (SimplygonSettings->PreferredRemesher == ERemeshingProcessorVersion::Default)
	{
		Simplygon::spRemeshingPipeline RemeshingPipeline = SDK->CreateRemeshingPipeline();

		Simplygon::spRemeshingSettings RemeshingSettings = RemeshingPipeline->GetRemeshingSettings();
		RemeshingSettings->SetOnScreenSize(InProxySettings.ScreenSize);
		RemeshingSettings->SetHoleFilling(Simplygon::EHoleFilling::Medium);
		RemeshingSettings->SetSurfaceTransferMode(Simplygon::ESurfaceTransferMode::Fast);
		//RemeshingSettings->SetKeepUnprocessedSceneMeshes(false);

		if (bHasClippingPlanes)
		{
			RemeshingPipeline->GetGeometryCullingSettings()->SetUseClippingPlanes(true);
			RemeshingPipeline->GetGeometryCullingSettings()->SetClippingPlaneSelectionSetName(SG_CLIPPING_PLANES_SELECTION_SET_NAME);
		}

		if (bHasClippingGeoemtry)
		{
			RemeshingPipeline->GetGeometryCullingSettings()->SetUseClippingGeometry(true);
			RemeshingPipeline->GetGeometryCullingSettings()->SetClippingGeometrySelectionSetName(SG_CLIPPING_GEOMETRY_SELECTION_SET_NAME);
		}

		Pipeline = Simplygon::spPipeline::SafeCast(RemeshingPipeline);
	}
	else
	{
		Simplygon::spRemeshingLegacyPipeline RemeshingPipeline = SDK->CreateRemeshingLegacyPipeline();

		// TODO add more settings back in
		Simplygon::spRemeshingLegacySettings RemeshingSettings = RemeshingPipeline->GetRemeshingLegacySettings();
		RemeshingSettings->SetOnScreenSize(InProxySettings.ScreenSize);
		RemeshingSettings->SetMergeDistance(InProxySettings.MergeDistance);
		bool bUseClippingGeometry = ClippingGeometrySet->GetItemCount() > 0 ? true : false;

		RemeshingSettings->SetUseClippingGeometry(bUseClippingGeometry);
		//RemeshingSettings->SetProcessSelectionSetName(SG_PROCSSING_SELECTION_SET_NAME);

		if (bUseClippingGeometry)
		{
			RemeshingSettings->SetClippingGeometrySelectionSetName(
				SG_CLIPPING_GEOMETRY_SELECTION_SET_NAME);
			RemeshingSettings->SetUseClippingGeometryEmptySpaceOverride(false);
		}

		if (bHasClippingPlanes)
		{
			RemeshingSettings->SetUseCuttingPlanes(true);
			RemeshingSettings->SetClippingGeometrySelectionSetName(SG_CLIPPING_PLANES_SELECTION_SET_NAME);
		}

		// setup surface transfer mode
		RemeshingSettings->SetSurfaceTransferMode(Simplygon::ESurfaceTransferMode::Fast);

		Pipeline = Simplygon::spPipeline::SafeCast(RemeshingPipeline);
	}

	// Setup material casters
	FSimplygonDataConverter::CreateMaterialCasters( SDK, Simplygon::spPipeline::SafeCast(Pipeline), InputMaterials );

	// Convert flatten materials to simplygon materials
	FSimplygonDataConverter::AddSimplygonMaterials(SDK, Scene, InputMaterials, true);

	// Setup mapping image
	Simplygon::spMappingImageSettings MappingImageSettings = Pipeline->GetMappingImageSettings();
	MappingImageSettings->SetGenerateMappingImage( true );
	MappingImageSettings->SetGenerateTexCoords( true );
	MappingImageSettings->SetGenerateTangents( true );
	// Once 18092 is resolved the input shoud be SG_MATERIALBAKING_INDEX
	MappingImageSettings->SetTexCoordLevel( SG_MATERIALBAKING_INDEX ); // Sets the output texcoord level.
	MappingImageSettings->GetOutputMaterialSettings( 0 )->SetTextureWidth( InProxySettings.MaterialSettings.TextureSize.X );
	MappingImageSettings->GetOutputMaterialSettings( 0 )->SetTextureHeight( InProxySettings.MaterialSettings.TextureSize.Y );
	MappingImageSettings->GetOutputMaterialSettings( 0 )->SetGutterSpace( (unsigned int)InProxySettings.MaterialSettings.GutterSpace );

	bool bSampleNavmeshVisibility = VisibilitySamplePoints.Num() > 0;

	const int32 Resolution = 512;

	if (bSampleNavmeshVisibility)
	{
		Simplygon::spSceneCamera SceneCamera = SDK->CreateSceneCamera();
		SceneCamera->SetCameraType(Simplygon::ECameraType::Omnidirectional);
		SceneCamera->SetFieldOfView(FMath::DegreesToRadians(90));
		SceneCamera->SetPixelFieldOfView(FMath::DegreesToRadians(90) / Resolution);

		// Get the camera position
		Simplygon::spRealArray CameraPositions = SceneCamera->GetCameraPositions();

		// Set the tuple count to 1
		CameraPositions->SetTupleCount(VisibilitySamplePoints.Num());

		for (auto Index = 0; Index < VisibilitySamplePoints.Num(); Index++)
		{
			VisibilitySamplePoints[Index] = FSimplygonDataConverter::VectorToSimplygon(VisibilitySamplePoints[Index]);
			float TempPoint[3] = { VisibilitySamplePoints[Index].X,VisibilitySamplePoints[Index].Y,VisibilitySamplePoints[Index].Z };
			CameraPositions->SetTuple(Index, TempPoint);
		}

		if (SceneCamera->ValidateCamera())
		{
			Scene->GetRootNode()->AddChild(SceneCamera);
			CameraSet->AddItem(SceneCamera->GetNodeGUID());
			Scene->GetSelectionSetTable()->AddItem(CameraSet);
		}

		if (Pipeline->IsA("IRemeshingPipeline"))
		{
			//does not support visibility for now
			FSimplygonDataConverter::SetupVisibilityCameraSetFromPipelineSettings<Simplygon::spRemeshingPipeline>(Pipeline,true);

		}
		else if (Pipeline->IsA("IRemeshingLegacyPipeline"))
		{
			FSimplygonDataConverter::SetupVisibilityCameraSetFromPipelineSettings<Simplygon::spRemeshingLegacyPipeline>(Pipeline,true);
		}
		else if (Pipeline->IsA("IAggregationPipeline"))
		{
			FSimplygonDataConverter::SetupVisibilityCameraSetFromPipelineSettings<Simplygon::spAggregationPipeline>(Pipeline,true);
		}
	}


	auto CompleteProxyLOD = [=](Simplygon::spScene Scene, const FSimplygonJobData& InJobData) {
#ifdef DEBUG_OUPUT_SIMPLYGON_SETTINGS
		DebugOutputRemeshingSettings(RemeshingProcessor->GetRemeshingSettings());
#endif

		// Collect the proxy mesh
		int32 SelectionSetId = Scene->SelectNodes("ISceneMesh");
		Simplygon::spSelectionSet SelectedMeshNodes =
			Scene->GetSelectionSetTable()->GetSelectionSet(SelectionSetId);
		Simplygon::spSceneMesh ProxyMesh = Simplygon::spSceneMesh::SafeCast(
			Scene->GetNodeByGUID(SelectedMeshNodes->GetItem(0)));

		if (ProxyMesh.IsNull())
		{
			FailedDelegate.ExecuteIfBound(InJobData.JobGuid, TEXT("Simplygon failed to generate a proxy mesh"));
		}
		else
		{
#ifdef DEBUG_PROXY_MESH
			OutputDebugGeometry(Scene, TEXT("AfterProxyMesh"));
#endif
			// Convert geometry data to raw mesh data
			TMap<int32, FName> EmptyMaterialMap;
			FMeshDescription OutProxyMesh;
			FStaticMeshAttributes(OutProxyMesh).Register();
			FSimplygonDataConverter::CreateMeshDescriptionFromGeometry(ProxyMesh->GetGeometry(), TMap<int32, FName>(), OutProxyMesh, true);
			if (OutProxyMesh.IsEmpty())
			{
				FailedDelegate.ExecuteIfBound(InJobData.JobGuid,
					TEXT("Simplygon failed to generate a valid proxy mesh"));
			}

			FFlattenMaterial BakedMaterial;
			if (!FSimplygonDataConverter::CreateFlattenMaterial(Scene, BakedMaterial)) 
			{
				FSimplygonDataConverter::AddDefaultDiffuseToMaterial( BakedMaterial );
			}

			BakedMaterial.FillAlphaValues(255);

			CompleteDelegate.ExecuteIfBound(OutProxyMesh, BakedMaterial, InJobData.JobGuid);
		}
	};

	if( SimplygonSettings->UseDistribution )
	{
		FSimplygonProcessProgressDelegate JobProgressDelegate;
		FSimplygonProcessCompleteDelegate JobCompleteDeletage;
		JobCompleteDeletage.BindLambda([=]( const FSimplygonJobData& InJobData, const Simplygon::spPipeline& Pipeline ) {
			CompleteProxyLOD(Pipeline->GetProcessedScene(), InJobData);
		});
		FSimplygonProcessFailedDelegate JobFailedDelegate;
		JobFailedDelegate.BindLambda([=](const FSimplygonJobData& InJobData, const FString& InError) {
			FailedDelegate.ExecuteIfBound(InJobData.JobGuid, InError);
		});

		FSimplygonRunnable* Runnable = new FSimplygonReduceStaticMeshRunnable(this, JobData, Scene, Pipeline, JobProgressDelegate, JobCompleteDeletage, JobFailedDelegate);
		Runnable->Thread = FRunnableThread::Create(Runnable, TEXT("FSimplygonReduction"));

		FScopeLock Lock(&StateLock);
		Runnables.Add(Runnable);
	}
	else
	{
		// Process data
		int32 LastWorkProgress = 0;
		FSimplygonProcessProgressDelegate JobProgressDelegate;
		JobProgressDelegate.BindLambda([&](const FSimplygonJobData& InJobData, int32 ProgressPercentage, int32 ProgressPercentageDelta)
			{
				if (ProgressPercentage > LastWorkProgress)
				{
					int32 Delta = ProgressPercentage - LastWorkProgress;
					SlowTask->EnterProgressFrame((float)Delta);
					LastWorkProgress = ProgressPercentage;
				}
			});

		RunPipeline(SDK, Simplygon::spPipeline::SafeCast(Pipeline), Scene, false, JobData, JobProgressDelegate);

		if( !WasPipelineSuccessful( Simplygon::spPipeline::SafeCast(Pipeline) ) )
		{
			FailedDelegate.ExecuteIfBound(InJobGUID, TEXT("Simplygon processing failed"));
		}
		else
		{
			CompleteProxyLOD(Scene, JobData);
        }
	}

	delete SlowTask;
}

FString FSimplygonReduction::GetName()
{
	return "SimplygonPluginMeshReduction";
}

//////////////////////////////////////////////////////////////////////////
// ISimplygonReduction
ISimplygonReduction* ISimplygonReduction::Create()
{
	return new FSimplygonReduction();
}

void FSimplygonReduction::RunSceneAsync(
	Simplygon::spScene SceneToProcess, 
	Simplygon::spPipeline Pipeline, 
	const FSimplygonJobData& JobData, 
	const FSimplygonProcessProgressDelegate& JobProgressDelegate, 
	const FSimplygonProcessCompleteDelegate& JobCompleteDelegate, 
	const FSimplygonProcessFailedDelegate& JobFailedDelegate)
{
	FSimplygonRunnable* Runnable = new FSimplygonReduceStaticMeshRunnable(this, JobData, SceneToProcess, Pipeline, JobProgressDelegate, JobCompleteDelegate, JobFailedDelegate);
	Runnable->Thread = FRunnableThread::Create(Runnable, TEXT("FSimplygonReduction"));

	FScopeLock Lock(&StateLock);
	Runnables.Add(Runnable);
}

void FSimplygonReduction::RunSceneSync(FSimplygonJobData& InJobData,
	Simplygon::spScene Scene,
	Simplygon::spPipeline Pipeline,
	const FSimplygonProcessProgressDelegate& JobProgressDelegate,
	const FSimplygonProcessCompleteDelegate& JobCompleteDelegate,
	const FSimplygonProcessFailedDelegate& JobFailedDelegate)
{
	Simplygon::ISimplygon* SDK = ISimplygonModule::Get().GetSDK(true);
	if (SDK == nullptr)
		return;

	RunPipeline(SDK, Simplygon::spPipeline::SafeCast(Pipeline), Scene, true, InJobData, JobProgressDelegate);

	if( !WasPipelineSuccessful(Simplygon::spPipeline::SafeCast(Pipeline)) )
	{
		// Processing failed
		JobFailedDelegate.Execute( InJobData, TEXT( "Pipeline execution failed" ) );
		return;
	}

	JobCompleteDelegate.Execute( InJobData, Pipeline );
}

void FSimplygonReduction::RunBatchAsync(Simplygon::spPipelineBatch Batch,
		const FSimplygonJobData& InJobData,
		const FSimplygonProcessProgressDelegate& JobProgressDelegate,
		const FSimplygonProcessBatchCompleteDelegate& JobCompleteDelegate,
		const FSimplygonProcessFailedDelegate& JobFailedDelegate)
{
	FSimplygonRunnable* Runnable = new FSimplygonPipelineBatchRunnable(this, InJobData, Batch, JobProgressDelegate, JobCompleteDelegate, JobFailedDelegate);
	Runnable->Thread = FRunnableThread::Create(Runnable, TEXT("FSimplygonReduction"));

	FScopeLock Lock(&StateLock);
	Runnables.Add(Runnable);
}

void FSimplygonReduction::RunBatchSync(const FSimplygonJobData& InJobData, 
		Simplygon::spPipelineBatch Batch,
		const FSimplygonProcessProgressDelegate& JobProgressDelegate,
		const FSimplygonProcessBatchCompleteDelegate& JobCompleteDelegate,
		const FSimplygonProcessFailedDelegate& JobFailedDelegate)
{
	Simplygon::ISimplygon* SDK = ISimplygonModule::Get().GetSDK(true);
	if (SDK == nullptr)
		return;

	RunPipelineBatch(SDK, Batch, true, InJobData, JobProgressDelegate);

	if( !WasPipelineBatchSuccessful(Batch) )
	{
		// Processing failed
		JobFailedDelegate.Execute( InJobData, TEXT( "Pipeline batch execution failed" ) );
		return;
	}

	JobCompleteDelegate.Execute( InJobData, Batch );
}

//////////////////////////////////////////////////////////////////////////

void FSimplygonReduction::ReduceSkeletalMeshWithResource(Simplygon::ISimplygon* SDK, USkeletalMesh* SkeletalMesh, FSkeletalMeshModel* SkeletalMeshResource, int32 LODIndex)
{
	// If the Current LOD is an import from file
	bool OldLodWasFromFile = SkeletalMesh->IsValidLODIndex(LODIndex) &&
		SkeletalMesh->GetLODInfo(LODIndex)->bHasBeenSimplified;

	// Insert a new LOD model entry if needed.
	if (LODIndex == SkeletalMeshResource->LODModels.Num())
	{
		SkeletalMeshResource->LODModels.Add(0);
	}

	// We'll need to store the max deviation after optimization if we wish to recalculate the LOD's display
	// distance
	float MaxDeviation = 0.0f;

	// Swap in a new model, delete the old.
	check(LODIndex < SkeletalMeshResource->LODModels.Num());
	FSkeletalMeshLODModel** LODModels = SkeletalMeshResource->LODModels.GetData();
	// delete LODModels[LODIndex]; -- keep model valid until we'll be ready to replace it; required to be able
	// to refresh UI with mesh stats

	// Copy over LOD info from LOD0 if there is no previous info.
	if (LODIndex == SkeletalMesh->GetLODNum())
	{
		// if there is no LOD, add one more
		SkeletalMesh->AddLODInfo();
	}

	// get settings
	const FSkeletalMeshOptimizationSettings& Settings =
		SkeletalMesh->GetLODInfo(LODIndex)->ReductionSettings;

	// select which mesh we're reducing from
	// use BaseLOD
	int32 BaseLOD = 0;
	FSkeletalMeshModel* SkelResource = SkeletalMesh->GetImportedModel();
	FSkeletalMeshLODModel* SrcModel = &SkelResource->LODModels[0];

	// only allow to set BaseLOD if the LOD is less than this
	if (Settings.BaseLOD > 0)
	{
		if (Settings.BaseLOD < LODIndex && SkeletalMeshResource->LODModels.IsValidIndex(Settings.BaseLOD))
		{
			BaseLOD = Settings.BaseLOD;
			SrcModel = &SkeletalMeshResource->LODModels[BaseLOD];
		}
		else
		{
			// warn users
			UE_LOG(LogSimplygon, Warning,
				TEXT("Building LOD %d - Invalid Base LOD entered. Using Base LOD 0"), LODIndex);
		}
	}

	// now try bone reduction process if it's setup
	TMap<FBoneIndexType, FBoneIndexType> BonesToRemove;

	IMeshBoneReductionModule& MeshBoneReductionModule =
		FModuleManager::Get().LoadModuleChecked<IMeshBoneReductionModule>("MeshBoneReduction");
	IMeshBoneReduction* MeshBoneReductionInterface = MeshBoneReductionModule.GetMeshBoneReductionInterface();

	TArray<FName> BoneNames;
	const int32 NumBones = SkeletalMesh->GetRefSkeleton().GetNum();
	for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
	{
		BoneNames.Add(SkeletalMesh->GetRefSkeleton().GetBoneName(BoneIndex));
	}

	// get the relative to ref pose matrices
	TArray<FMatrix> RelativeToRefPoseMatrices;
	RelativeToRefPoseMatrices.AddUninitialized(NumBones);
	// if it has bake pose, gets ref to local matrices using bake pose
	if (const UAnimSequence* BakePoseAnim = SkeletalMesh->GetLODInfo(LODIndex)->BakePose)
	{
		TArray<FTransform> BonePoses;
		UAnimationBlueprintLibrary::GetBonePosesForFrame(BakePoseAnim, BoneNames, 0, true, BonePoses,
			SkeletalMesh);

		const FReferenceSkeleton& RefSkeleton = SkeletalMesh->GetRefSkeleton();
		const TArray<FTransform>& RefPoseInLocal = RefSkeleton.GetRefBonePose();

		// get component ref pose
		TArray<FTransform> RefPoseInCS;
		FAnimationRuntime::FillUpComponentSpaceTransforms(RefSkeleton, RefPoseInLocal, RefPoseInCS);

		// calculate component space bake pose
		TArray<FMatrix> ComponentSpacePose, ComponentSpaceRefPose, AnimPoseMatrices;
		ComponentSpacePose.AddUninitialized(NumBones);
		ComponentSpaceRefPose.AddUninitialized(NumBones);
		AnimPoseMatrices.AddUninitialized(NumBones);

		// to avoid scale issue, we use matrices here
		for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
		{
			ComponentSpaceRefPose[BoneIndex] = RefPoseInCS[BoneIndex].ToMatrixWithScale();
			AnimPoseMatrices[BoneIndex] = BonePoses[BoneIndex].ToMatrixWithScale();
		}

		for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
		{
			const int32 ParentIndex = RefSkeleton.GetParentIndex(BoneIndex);
			if (ParentIndex != INDEX_NONE)
			{
				ComponentSpacePose[BoneIndex] =
					AnimPoseMatrices[BoneIndex] * ComponentSpacePose[ParentIndex];
			}
			else
			{
				ComponentSpacePose[BoneIndex] = AnimPoseMatrices[BoneIndex];
			}
		}

		// calculate relative to ref pose transform and convert to matrices
		for (int32 BoneIndex = 0; BoneIndex < NumBones; ++BoneIndex)
		{
			RelativeToRefPoseMatrices[BoneIndex] =
				ComponentSpaceRefPose[BoneIndex].Inverse() * ComponentSpacePose[BoneIndex];
		}
	}
	else
	{
		for (int32 Index = 0; Index < NumBones; ++Index)
		{
			RelativeToRefPoseMatrices[Index] = FMatrix::Identity;
		}
	}

	// Swap in a new model, delete the old.
	FSkeletalMeshLODModel* NewModel = new FSkeletalMeshLODModel();
	FSkeletalMeshLODModel* OldLODModel = LODModels[LODIndex];
	LODModels[LODIndex] = NewModel;
	if (OldLODModel)
	{
		if (!Settings.OnDeleteLODModelDelegate.IsBound())
		{
			//If not in game thread we should never delete a structure containing bulkdata since it can crash when the bulkdata is detach from the archive
			//Use the delegate and delete the pointer in the main thread if you reduce in other thread then game thread (main thread).
			check(IsInGameThread());
			delete OldLODModel;
		}
		else
		{
			Settings.OnDeleteLODModelDelegate.Execute(OldLODModel);
		}
	}

	// Reduce LOD model with SrcMesh
	if (ReduceSkeletalLODModel(SDK, SrcModel, NewModel, SkeletalMesh, MaxDeviation, Settings, RelativeToRefPoseMatrices, LODIndex))
	{
		// See if we'd like to remove extra bones first
		if (MeshBoneReductionInterface->GetBoneReductionData(SkeletalMesh, LODIndex, BonesToRemove))
		{
			// fix up chunks to remove the bones that set to be removed
			for (int32 SectionIndex = 0; SectionIndex < NewModel->Sections.Num(); ++SectionIndex)
			{
				MeshBoneReductionInterface->FixUpSectionBoneMaps(NewModel->Sections[SectionIndex], BonesToRemove, NewModel->SkinWeightProfiles);
			}
		}

		if (OldLodWasFromFile)
		{
			SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap.Empty();
		}

		// If base lod has a customized LODMaterialMap and this LOD doesn't (could have if changes are applied
		// instead of freshly generated, copy over the data into new new LOD
		if (SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap.Num() == 0 &&
			SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap.Num() != 0)
		{
			SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap =
				SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap;
		}
		else
		{
			// Assuming the reducing step has set all material indices correctly, we double check if something
			// went wrong make sure we don't have more materials
			int32 TotalSectionCount = NewModel->Sections.Num();
			if (SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap.Num() > TotalSectionCount)
			{
				SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap =
					SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap;
				// Something went wrong during the reduce step during regenerate
				check(SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap.Num() == TotalSectionCount);
			}
		}

		// Flag this LOD as having been simplified.
		SkeletalMesh->GetLODInfo(LODIndex)->bHasBeenSimplified = true;
		SkeletalMesh->SetHasBeenSimplified(true);
	}
	else
	{
		FSkeletalMeshLODModel::CopyStructure(NewModel, SrcModel);

		// See if we'd like to remove extra bones first
		if (MeshBoneReductionInterface->GetBoneReductionData(SkeletalMesh, LODIndex, BonesToRemove))
		{
			// fix up chunks to remove the bones that set to be removed
			for (int32 SectionIndex = 0; SectionIndex < NewModel->Sections.Num(); ++SectionIndex)
			{
				MeshBoneReductionInterface->FixUpSectionBoneMaps(NewModel->Sections[SectionIndex], BonesToRemove, NewModel->SkinWeightProfiles);
			}
		}

		// Clean up some section data
		for (int32 SectionIndex = SrcModel->Sections.Num() - 1; SectionIndex >= 0; --SectionIndex)
		{
			// New model should be reset to -1 value
			if(NewModel->Sections.IsValidIndex(SectionIndex))
				NewModel->Sections[SectionIndex].GenerateUpToLodIndex = -1;
			
			int8 GenerateUpToLodIndex = SrcModel->Sections[SectionIndex].GenerateUpToLodIndex;
			if (GenerateUpToLodIndex != -1 && GenerateUpToLodIndex < LODIndex)
			{
				// Remove the section
				RemoveSkeletalMeshSection(NewModel, SectionIndex);
			}
		}

		SkeletalMesh->GetLODInfo(LODIndex)->LODMaterialMap =
			SkeletalMesh->GetLODInfo(BaseLOD)->LODMaterialMap;
		// Required bones are recalculated later on.
		NewModel->RequiredBones.Empty();
		SkeletalMesh->GetLODInfo(LODIndex)->bHasBeenSimplified = true;
		SkeletalMesh->SetHasBeenSimplified(true);
	}

	SkeletalMesh->CalculateRequiredBones(SkeletalMeshResource->LODModels[LODIndex],
		SkeletalMesh->GetRefSkeleton(), &BonesToRemove);
}

bool FSimplygonReduction::ReduceSkeletalLODModel(
	Simplygon::ISimplygon* SDK, 
	FSkeletalMeshLODModel* SrcModel, 
	FSkeletalMeshLODModel*& OutModel,
	USkeletalMesh* SkeletalMesh,
	float& MaxDeviation, 
	const FSkeletalMeshOptimizationSettings& MeshOptimizationSettings, 
	const TArray<FMatrix>& BoneMatrices, 
	const int32 LODIndex)
{
	UE_LOG(LogSimplygon, Log, TEXT("Begin ReduceLODModel"));

	int32 LastWorkProgress = 0;
	FScopedSlowTask SlowTask(100, (LOCTEXT("SimplygonReduction_ReduceMeshDescription", "Simplygon generating LOD")));
	SlowTask.MakeDialog();

	const bool bUsingMaxDeviation = (MeshOptimizationSettings.ReductionMethod != SMOT_NumOfTriangles &&
		MeshOptimizationSettings.MaxDeviationPercentage > 0.0f);
	const bool bUsingReductionRatio = (MeshOptimizationSettings.ReductionMethod != SMOT_MaxDeviation &&
		MeshOptimizationSettings.NumOfTrianglesPercentage < 1.0f);
	const bool bProcessGeometry = (bUsingMaxDeviation || bUsingReductionRatio);
	const bool bProcessBones = (MeshOptimizationSettings.MaxBonesPerVertex < MAX_TOTAL_INFLUENCES);
	const bool bOptimizeMesh = (bProcessGeometry || bProcessBones);

	const FBoxSphereBounds& Bounds = SkeletalMesh->GetImportedBounds();

	// We'll need to store the max deviation after optimization if we wish to recalculate the LOD's display
	// distance
	MaxDeviation = 0.0f;

	if (bOptimizeMesh)
	{
		// Create a simplygon scene. The scene by default contains a bone table that is required for bone
		// reduction.
		Simplygon::spScene Scene = SDK->CreateScene();
		check(Scene);

		// Create bone hierarchy for simplygon.
		TArray<Simplygon::rid> BoneTableIDs;
		FSimplygonDataConverter::CreateSkeletalHierarchy(SDK, Scene, SkeletalMesh->GetRefSkeleton(), BoneTableIDs);

		// Create a new scene mesh object
		Simplygon::spGeometryData GeometryData =
			FSimplygonDataConverter::CreateGeometryFromSkeletalLODModel(SDK, Scene, *SrcModel, BoneTableIDs, BoneMatrices, LODIndex);

		Simplygon::spReductionPipeline ReductionPipeline = SDK->CreateReductionPipeline();

		Simplygon::spRepairSettings RepairSettings = ReductionPipeline->GetRepairSettings();
		RepairSettings->SetWeldDist(MeshOptimizationSettings.WeldingThreshold);
		RepairSettings->SetTJuncDist(MeshOptimizationSettings.WeldingThreshold);

		Simplygon::spReductionSettings ReductionSettings = ReductionPipeline->GetReductionSettings();
		SetSkeletalReductionSettings(MeshOptimizationSettings, Bounds.SphereRadius,
			GeometryData->GetTriangleCount(), ReductionSettings);

		Simplygon::spNormalCalculationSettings NormalSettings =
			ReductionPipeline->GetNormalCalculationSettings();
		SetSkeletalNormalSettings(MeshOptimizationSettings, NormalSettings);

		const double StartTime = FPlatformTime::Seconds();
		UE_LOG(LogSimplygon, Log, TEXT("Processing with %s"), *FString(ReductionPipeline->GetClass().c_str()));


		FSimplygonProcessProgressDelegate JobProgressDelegate;
		JobProgressDelegate.BindLambda([&](const FSimplygonJobData& InJobData, int32 ProgressPercentage, int32 ProgressPercentageDelta)
			{
				if (ProgressPercentage > LastWorkProgress)
				{
					int32 Delta = ProgressPercentage - LastWorkProgress;
					SlowTask.EnterProgressFrame((float)Delta);
					LastWorkProgress = ProgressPercentage;
				}
			});

		TArray<FFlattenMaterial> NoMaterials;
		RunPipeline(SDK, Simplygon::spPipeline::SafeCast(ReductionPipeline), Scene, false, FSimplygonJobData(), JobProgressDelegate);

		if (!WasPipelineSuccessful(Simplygon::spPipeline::SafeCast(ReductionPipeline)))
		{
			// Processing failed
			UE_LOG(LogSimplygon, Error, TEXT("Pipeline execution failed"));
			return false;
		}

		// We require the max deviation if we're calculating the LOD's display distance.
		MaxDeviation = ReductionPipeline->GetResultDeviation();

		FSimplygonDataConverter::CreateSkeletalLODModelFromGeometry(OutModel, GeometryData, SkeletalMesh, LODIndex, false);

		// return true if it created any vertice
		return (OutModel->NumVertices > 0);
	}

	return false;
}

bool FSimplygonReduction::RemoveSkeletalMeshSection(FSkeletalMeshLODModel* Model, int32 SectionIndex)
{
	// Need a valid section
	if (!Model->Sections.IsValidIndex(SectionIndex))
	{
		return false;
	}

	FSkelMeshSection& SectionToRemove = Model->Sections[SectionIndex];

	if (SectionToRemove.CorrespondClothAssetIndex != INDEX_NONE)
	{
		// Can't remove this, clothing currently relies on it
		return false;
	}

	const uint32 NumVertsToRemove = SectionToRemove.GetNumVertices();
	const uint32 BaseVertToRemove = SectionToRemove.BaseVertexIndex;
	const uint32 NumIndicesToRemove = SectionToRemove.NumTriangles * 3;
	const uint32 BaseIndexToRemove = SectionToRemove.BaseIndex;

	// Strip indices
	Model->IndexBuffer.RemoveAt(BaseIndexToRemove, NumIndicesToRemove);

	Model->Sections.RemoveAt(SectionIndex);

	// Fixup indices above base vert
	for (uint32& Index : Model->IndexBuffer)
	{
		if (Index >= BaseVertToRemove)
		{
			Index -= NumVertsToRemove;
		}
	}

	Model->NumVertices -= NumVertsToRemove;

	// Fixup anything needing section indices
	for (FSkelMeshSection& Section : Model->Sections)
	{
		// Push back clothing indices
		if (Section.CorrespondClothAssetIndex > SectionIndex)
		{
			Section.CorrespondClothAssetIndex--;
		}

		// Removed indices, rebase further sections
		if (Section.BaseIndex > BaseIndexToRemove)
		{
			Section.BaseIndex -= NumIndicesToRemove;
		}

		// Remove verts, rebase further sections
		if (Section.BaseVertexIndex > BaseVertToRemove)
		{
			Section.BaseVertexIndex -= NumVertsToRemove;
		}
	}
	return true;
}

bool FSimplygonReduction::IsUsingIncrediBuild() const
{
	const USimplygonSettings* SimplygonSettings = GetDefault<USimplygonSettings>();
	return SimplygonSettings->UseDistribution && ( SimplygonSettings->DistributionMethod == EDistributionMethod::Incredibuild);
}

void FSimplygonReduction::RunPipeline(
	Simplygon::ISimplygon* SDK,
	Simplygon::spPipeline Pipeline,
	Simplygon::spScene Scene,
	bool AllowDistributionOfThisRun,
	const FSimplygonJobData& JobData,
	const FSimplygonProcessProgressDelegate& ProgressDelegate)
{
	UE_LOG(LogSimplygon, Log, TEXT("Simplygon pipeline executing .... "));

	FDefaultEventHandler EventHandler(JobData, ProgressDelegate);
	Pipeline->AddObserver(&EventHandler);

	const USimplygonSettings* SimplygonSettings = GetDefault<USimplygonSettings>();
	Simplygon::EPipelineRunMode RunMode = Simplygon::EPipelineRunMode::RunInThisProcess;
	if(AllowDistributionOfThisRun && SimplygonSettings->UseDistribution)
	{
		RunMode = Simplygon::EPipelineRunMode::RunDistributedUsingSimplygonGrid;
		if(SimplygonSettings->DistributionMethod == EDistributionMethod::FastBuild)
			RunMode = Simplygon::EPipelineRunMode::RunDistributedUsingFastbuild;
		else if(SimplygonSettings->DistributionMethod == EDistributionMethod::Incredibuild)
			RunMode = Simplygon::EPipelineRunMode::RunDistributedUsingIncredibuild;
	}
	Pipeline->GetPipelineSettings()->SetCascadedRunMode(RunMode);

	// Need to set batch path for each pipeline if we are using the local sdk path
	ISimplygonModule& SimplygonSDKModule = FModuleManager::Get().LoadModuleChecked<ISimplygonModule>("Simplygon");
	if (SimplygonSDKModule.IsUsingLocalSdkPath())
	{
		FString SimplygonSdkPath = SimplygonSDKModule.GetSimplygonSDKPath();
		auto RecursevlySetBatchPath = [&SimplygonSdkPath](Simplygon::spPipeline InPipeline, const auto& SetBatchPathFunc)->void
		{
			InPipeline->GetPipelineSettings()->SetSimplygonBatchPath(TCHAR_TO_ANSI(*SimplygonSdkPath));

			int32 NumChildPipelines = InPipeline->GetCascadedPipelineCount();
			for (int32 ChildIndex = 0; ChildIndex < NumChildPipelines; ++ChildIndex)
			{
				SetBatchPathFunc(Simplygon::spPipeline::SafeCast(InPipeline->GetCascadedPipelineByIndex(ChildIndex)), SetBatchPathFunc);
			}
		};
		RecursevlySetBatchPath(Pipeline, RecursevlySetBatchPath);
	}

	const USimplygonSettings* EditorSettings = GetDefault< USimplygonSettings>();
	if (EditorSettings->bDebug && EditorSettings->bExportPipelineAndScene)
	{
		FSimplygonDataConverter::SaveSimplygonPipelineToIntermediate(SDK, Pipeline, TEXT("SG_UE4_Input"), JobData.JobGuid);
		FSimplygonDataConverter::SaveSimplygonSceneToIntermediate(SDK, Scene, TEXT("SG_UE4_Input"), JobData.JobGuid);
		
	}

	double ProcessingTime = 0.0f;
	UE_LOG(LogSimplygon, Log, TEXT("Processing with %s"), *FString(Pipeline->GetClass().c_str()));
	{
		FScopedDurationTimer ProcessingTimer(ProcessingTime);
		RESTORE_FP_CTRL_WORD_MCW_EM(Pipeline->RunScene(Scene, RunMode));
	}
	UE_LOG(LogSimplygon, Log, TEXT("Processing done in %.2fs"), ProcessingTime);

	if( EditorSettings->bDebug && EditorSettings->bExportPipelineAndScene )
	{
		if (WasPipelineSuccessful(Pipeline)) {
			auto RecursevlyExportResults = [ &]( Simplygon::spPipeline InPipeline, const auto& SetRecurseFunction, FGuid Id, int index = 0 ) -> void {
	
				FString OutputPrefix = FString::Printf( TEXT( "SG_UE4_Output%s_LOD%d" ), *Id.ToString().Left( 8 ) , index );
				if(!InPipeline->GetProcessedScene().IsNull())
					FSimplygonDataConverter::SaveSimplygonSceneToIntermediate( SDK, InPipeline->GetProcessedScene(), OutputPrefix, JobData.JobGuid );

				int32 NumChildPipelines = InPipeline->GetCascadedPipelineCount();
				if (NumChildPipelines > 0) {
					FGuid ParentId = FGuid::NewGuid();
					for( int32 ChildIndex = 0; ChildIndex < NumChildPipelines; ++ChildIndex )
					{
						SetRecurseFunction( Simplygon::spPipeline::SafeCast( InPipeline->GetCascadedPipelineByIndex( ChildIndex )),
						                    SetRecurseFunction,
						                    ParentId,
						                    ChildIndex );
					}
				}
			};
			RecursevlyExportResults( Pipeline, RecursevlyExportResults, FGuid::NewGuid(), 0 );	
		}
	}
}

//extern "C" long WINAPI SetEnvironmentVariableA(const char*, const char*);

void FSimplygonReduction::RunPipelineBatch(
	Simplygon::ISimplygon* SDK,
	Simplygon::spPipelineBatch Batch,
	bool AllowDistributionOfThisRun,
	const FSimplygonJobData& JobData,
	const FSimplygonProcessProgressDelegate& ProgressDelegate)
{
	UE_LOG(LogSimplygon, Log, TEXT("Simplygon pipeline batch executing .... "));

	FDefaultEventHandler EventHandler(JobData, ProgressDelegate);
	Batch->AddObserver(&EventHandler);

	const USimplygonSettings* SimplygonSettings = GetDefault<USimplygonSettings>();
	Simplygon::EPipelineRunMode RunMode = Simplygon::EPipelineRunMode::RunInThisProcess;
	if(AllowDistributionOfThisRun && SimplygonSettings->UseDistribution)
	{
		RunMode = Simplygon::EPipelineRunMode::RunDistributedUsingSimplygonGrid;
		if(SimplygonSettings->DistributionMethod == EDistributionMethod::FastBuild)
			RunMode = Simplygon::EPipelineRunMode::RunDistributedUsingFastbuild;
		else if(SimplygonSettings->DistributionMethod == EDistributionMethod::Incredibuild)
			RunMode = Simplygon::EPipelineRunMode::RunDistributedUsingIncredibuild;
	}

	// Need to set batch path if we are using the local sdk path
	ISimplygonModule& SimplygonSDKModule = FModuleManager::Get().LoadModuleChecked<ISimplygonModule>("Simplygon");
	if (SimplygonSDKModule.IsUsingLocalSdkPath())
	{
		const char* batchPath = TCHAR_TO_ANSI(*SimplygonSDKModule.GetSimplygonSDKPath());
		// TODO: Needs the simplygon batch path setting on pipeline batch from 9.2
		// Batch->SetSimplygonBatchPath(batchPath);
		// For now, use the env var
		SetEnvironmentVariableA("SIMPLYGON_9_PATH", batchPath);
	}

	double ProcessingTime = 0.0f;
	{
		FScopedDurationTimer ProcessingTimer(ProcessingTime);
		RESTORE_FP_CTRL_WORD_MCW_EM(Batch->Run(RunMode));
	}
	UE_LOG(LogSimplygon, Log, TEXT("Processing done in %.2fs"), ProcessingTime);
}

void FSimplygonReduction::SetReductionSettings(Simplygon::spReductionSettings ReductionSettings, const FMeshReductionSettings& Settings, int32 NumTris)
{
	float MaxDeviation = Settings.MaxDeviation > 0.0f ? Settings.MaxDeviation : Simplygon::REAL_MAX;
	float MinReductionRatio = FMath::Max<float>(1.0f / NumTris, 0.05f);
	float MaxReductionRatio =
		(Settings.MaxDeviation > 0.0f && Settings.PercentTriangles == 1.0f) ? MinReductionRatio : 1.0f;
	float ReductionRatio = FMath::Clamp(Settings.PercentTriangles, MinReductionRatio, MaxReductionRatio);

	const float ImportanceTable[] = {
		0.0f,   // OFF
		0.125f, // Lowest
		0.35f,  // Low,
		1.0f,   // Normal
		2.8f,   // High
		8.0f,   // Highest
	};

	static_assert(UE_ARRAY_COUNT(ImportanceTable) == (EMeshFeatureImportance::Highest + 1),
		"Importance table size mismatch."); // -1 because of TEMP_BROKEN(?)
	check(Settings.SilhouetteImportance <
		EMeshFeatureImportance::Highest + 1);                              // -1 because of TEMP_BROKEN(?)
	check(Settings.TextureImportance < EMeshFeatureImportance::Highest + 1); // -1 because of TEMP_BROKEN(?)
	check(Settings.ShadingImportance < EMeshFeatureImportance::Highest + 1); // -1 because of TEMP_BROKEN(?)
	check(Settings.VertexColorImportance <
		EMeshFeatureImportance::Highest + 1); // -1 because of TEMP_BROKEN(?)

	ReductionSettings->SetReductionTargets(Simplygon::EStopCondition::Any, true, false, true, false);
	ReductionSettings->SetReductionTargetMaxDeviation(MaxDeviation);
	ReductionSettings->SetReductionTargetTriangleRatio(ReductionRatio);
	ReductionSettings->SetGeometryImportance(ImportanceTable[Settings.SilhouetteImportance]);
	ReductionSettings->SetTextureImportance(ImportanceTable[Settings.TextureImportance]);
	ReductionSettings->SetMaterialImportance(ImportanceTable[Settings.TextureImportance]);
	ReductionSettings->SetShadingImportance(ImportanceTable[Settings.ShadingImportance]);
	ReductionSettings->SetVertexColorImportance(ImportanceTable[Settings.VertexColorImportance]);

	// Automatic Symmetry Detection
	ReductionSettings->SetKeepSymmetry(Settings.bKeepSymmetry);
	ReductionSettings->SetUseAutomaticSymmetryDetection(Settings.bKeepSymmetry);

	ReductionSettings->SetReductionHeuristics(Simplygon::EReductionHeuristics::Fast);
	ReductionSettings->SetDataCreationPreferences(
		Simplygon::EDataCreationPreferences::PreferOriginalData);
	ReductionSettings->SetGenerateGeomorphData(true);
}

void FSimplygonReduction::SetNormalSettings(Simplygon::spNormalCalculationSettings NormalSettings, const FMeshReductionSettings& Settings)
{
	NormalSettings->SetReplaceNormals(Settings.bRecalculateNormals);
	NormalSettings->SetScaleByArea(false);
	NormalSettings->SetScaleByAngle(false);
	NormalSettings->SetHardEdgeAngle(FMath::DegreesToRadians(Settings.HardAngleThreshold));
}

void FSimplygonReduction::SetVisibilitySettings(Simplygon::spVisibilitySettings VisibiliySettings)
{
	VisibiliySettings->SetCameraSelectionSetName(SG_VISIBILITY_SELECTION_SET_NAME);
	VisibiliySettings->SetUseVisibilityWeightsInReducer(true);
	VisibiliySettings->SetUseVisibilityWeightsInTexcoordGenerator(true);
	VisibiliySettings->SetCullOccludedGeometry(false);
	VisibiliySettings->SetOccluderSelectionSetName(SG_OCCLUDER_GEOMETRY_SELECTION_SET_NAME);
	VisibiliySettings->SetFillNonVisibleAreaThreshold(false);
	// VisibiliySettings->SetRemoveTrianglesNotOccludingOtherTriangles(SimplygonSettings->bRemoveTrianglesNotOccluingOtherTriangles);
	VisibiliySettings->SetUseBackfaceCulling(true);
	VisibiliySettings->SetConservativeMode(true);
}

void FSimplygonReduction::SetSkeletalReductionSettings(const FSkeletalMeshOptimizationSettings& Settings, float BoundsRadius, int32 SourceTriCount, Simplygon::spReductionSettings ReductionSettings)
{
	// Compute max deviation from quality.
	float MaxDeviation = (Settings.ReductionMethod != SMOT_NumOfTriangles)
		? Settings.MaxDeviationPercentage * BoundsRadius
		: Simplygon::REAL_MAX;

	// Set the reduction ratio such that at least 1 triangle or 1% of the original triangles remain, whichever
	// is larger.
	float MinReductionRatio = FMath::Max<float>(1.0f / SourceTriCount, 0.01f);
	float ReductionRatio = MinReductionRatio;

	// if reduction is not deviate, we set the percentage
	if (Settings.ReductionMethod != SMOT_MaxDeviation)
	{
		ReductionRatio = FMath::Clamp(Settings.NumOfTrianglesPercentage, MinReductionRatio, 1.f);
	}

	const float ImportanceTable[] = {
		0.0f,   // OFF
		0.125f, // Lowest
		0.35f,  // Low,
		1.0f,   // Normal
		2.8f,   // High
		8.0f,   // Highest
	};

	static_assert(UE_ARRAY_COUNT(ImportanceTable) == SMOI_MAX, "Bad importance table size.");
	check(Settings.SilhouetteImportance < SMOI_MAX);
	check(Settings.TextureImportance < SMOI_MAX);
	check(Settings.ShadingImportance < SMOI_MAX);
	check(Settings.SkinningImportance < SMOI_MAX);

	switch (Settings.ReductionMethod)
	{
	case SMOT_TriangleOrDeviation:
		ReductionSettings->SetReductionTargets(Simplygon::EStopCondition::Any, true, false, true, false);
		break;
	case SMOT_NumOfTriangles: 
		ReductionSettings->SetReductionTargets(Simplygon::EStopCondition::Any, true, false, false, false);
		break;
	case SMOT_MaxDeviation: 
		ReductionSettings->SetReductionTargets(Simplygon::EStopCondition::Any, false, false, true, false);
		break;
	default: 
		ReductionSettings->SetReductionTargets(Simplygon::EStopCondition::Any, true, false, false, false);
		break;
	}

	ReductionSettings->SetReductionTargetMaxDeviation(MaxDeviation);
	ReductionSettings->SetReductionTargetTriangleRatio(ReductionRatio);
	ReductionSettings->SetGeometryImportance(ImportanceTable[Settings.SilhouetteImportance]);
	ReductionSettings->SetTextureImportance(ImportanceTable[Settings.TextureImportance]);
	ReductionSettings->SetMaterialImportance(ImportanceTable[Settings.TextureImportance]);
	ReductionSettings->SetShadingImportance(ImportanceTable[Settings.ShadingImportance]);
	ReductionSettings->SetSkinningImportance(ImportanceTable[Settings.SkinningImportance]);
}

void FSimplygonReduction::SetSkeletalNormalSettings(const FSkeletalMeshOptimizationSettings& Settings, Simplygon::spNormalCalculationSettings NormalSettings)
{
	NormalSettings->SetReplaceNormals(Settings.bRecalcNormals);
	NormalSettings->SetScaleByArea(false);
	NormalSettings->SetScaleByAngle(false);
	NormalSettings->SetHardEdgeAngle(FMath::DegreesToRadians(Settings.NormalsThreshold));
}

void FSimplygonReduction::SetBoneSettings(const FSkeletalMeshOptimizationSettings& Settings, Simplygon::spBoneSettings BoneSettings)
{
	BoneSettings->SetBoneReductionTargets(Simplygon::EStopCondition::Any, true, false, false, false);
	BoneSettings->SetMaxBonePerVertex(Settings.MaxBonesPerVertex);
	BoneSettings->SetLimitBonesPerVertex(true);
	BoneSettings->SetUseBoneReducer(true);
}

bool FSimplygonReduction::ValidateGeometry(Simplygon::ISimplygon* SDK, Simplygon::spGeometryValidator& GeometryValidator, Simplygon::spGeometryData& GeometryData)
{
	GeometryData->CleanupInvalidTriangles();
	GeometryData->CleanupNanValues();

	bool bGeometryValid = GeometryValidator->ValidateGeometry(GeometryData);
	if (!bGeometryValid)
	{
		uint32 error_val = GeometryValidator->GetErrorValue();
		if ((error_val & (uint32)Simplygon::EGeometryValidationCodes::ZeroLengthNormal) != 0)
		{
			Simplygon::spNormalRepairer rep = SDK->CreateNormalRepairer();
			rep->SetRepairOnlyInvalidNormals(true);
			rep->SetGeometry(GeometryData);
			RESTORE_FP_CTRL_WORD_MCW_EM(rep->RunProcessing());
		}
		else
		{
			FString ErrorMessage = ANSI_TO_TCHAR(GeometryValidator->GetErrorString());

			// Downgrade geometry validation errors to info
			ErrorMessage = ErrorMessage.Replace(TEXT("Error:"), TEXT("Info:"));

			UE_LOG(LogSimplygon, Log, TEXT("Invalid mesh data: %s."), *ErrorMessage);
		}
	}

	return bGeometryValid;
}

#undef LOCTEXT_NAMESPACE
