// Fill out your copyright notice in the Description page of Project Settings.

#include "OptiXLensComponent.h"

#include "Runtime/Engine/Classes/Engine/TextureRenderTargetCube.h"


#include "OptiXModule.h"

UOptiXLensComponent::UOptiXLensComponent(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	Radius1 = 0.8 * 100; // todo default values
	Radius2 = 1.0 * 100;
	LensRadius = 0.1 * 100;

	LensType1 = ELensSideType::CONVEX;
	LensType2 = ELensSideType::CONVEX;
	LensThickness = 0.025 * 100;
	CurrentWavelength = 450.0f;

	// A little hacky but w/e
	for (const TPair<FString, FGlassDefinition>& Pair : FOptiXModule::Get().GetGlassDefinitions())
	{
		GlassType = Pair.Key;
		break;
	}
	
}


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


	// do this on the game thread
	// hook into WL update:
	//UE_LOG(LogTemp, Display, TEXT("Begin Play on LensComponent, GameThread"));

	FOptiXModule::Get().GetOptiXContextManager()->WavelengthChangedEvent.AddUFunction(this, "OnWavelengthChangedEvent");
}


void UOptiXLensComponent::UpdateOptiXComponentVariables()
{
	check(IsInRenderingThread());


	OptiXGeometryInstance->SetFloat("radius", Radius1);
	//UE_LOG(LogTemp, Display, TEXT("radius1 : %f"), Radius1);

	OptiXGeometryInstance->SetFloat("radius2", Radius2);
	//UE_LOG(LogTemp, Display, TEXT("radius2 : %f"), Radius2);

	OptiXGeometryInstance->SetFloat("lensRadius", LensRadius);
	//UE_LOG(LogTemp, Display, TEXT("lensradius : %f"), LensRadius);

	OptiXGeometryInstance->SetFloat("halfCylinderLength", GetCylinderLength(LensThickness) / 2.0f);
	//UE_LOG(LogTemp, Display, TEXT("halfCylinderLength : %f"), GetCylinderLength(LensThickness) / 2.0f);

	OptiXGeometryInstance->SetInt("side1Type", static_cast<int32>(LensType1));
	//UE_LOG(LogTemp, Display, TEXT("type1 : %i"), static_cast<int32>(LensType1));

	OptiXGeometryInstance->SetInt("side2Type", static_cast<int32>(LensType2));
	//UE_LOG(LogTemp, Display, TEXT("type2 : %i"), static_cast<int32>(LensType2));


	double WL2 = FMath::Pow(CurrentWavelength / 1000.0, 2.0f);
	FGlassDefinition Def = FOptiXModule::Get().GetGlassDefinitions()[GlassType];
	//UE_LOG(LogTemp, Display, TEXT("Glass Def: %f, %f, %F"), Def.B.X, Def.B.Y, Def.B.Z);

	float Index = FMath::Sqrt(1 +
		Def.B.X * WL2 / (WL2 - Def.C.X) +
		Def.B.Y * WL2 / (WL2 - Def.C.Y) +
		Def.B.Z * WL2 / (WL2 - Def.C.Z));

	OptiXMaterial->SetFloat("refraction_index", Index);

	MarkDirty();
}


void UOptiXLensComponent::UpdateOptiXComponent()
{
	if (OptiXGeometry != nullptr && OptiXTransform != nullptr && OptiXAcceleration != nullptr)
	{
		FMatrix T = GetComponentToWorld().ToMatrixNoScale();
		OptiXTransform->SetMatrix(T);
		//OptiXAcceleration->MarkDirty();
		OptiXContext->GetGroup("top_object")->GetAcceleration()->MarkDirty();
		FOptiXModule::Get().GetOptiXContextManager()->OnSceneChangedDelegate.Broadcast();
	}
}

void UOptiXLensComponent::InitOptiXGeometry()
{
	OptiXGeometry = OptiXContext->CreateGeometry();
	OptiXGeometry->SetPrimitiveCount(1u);
	UOptiXProgram* BB = OptiXContext->CreateProgramFromPTXFile(OptiXPTXDir + "generated/lens_parametric.ptx", "bounds");
	UOptiXProgram* IP = OptiXContext->CreateProgramFromPTXFile(OptiXPTXDir + "generated/lens_parametric.ptx", "intersect");

	OptiXGeometry->SetBoundingBoxProgram(BB);
	OptiXGeometry->SetIntersectionProgram(IP);
}

void UOptiXLensComponent::InitOptiXMaterial()
{
	UOptiXProgram* CHPerspective = OptiXContext->CreateProgramFromPTXFile(OptiXPTXDir + "generated/glass_perspective_camera.ptx", "closest_hit_radiance");
	UOptiXProgram* CHIterative = OptiXContext->CreateProgramFromPTXFile(OptiXPTXDir + "generated/glass_iterative_camera.ptx", "closest_hit_radiance");

	OptiXMaterial = OptiXContext->CreateMaterial();
	OptiXMaterial->SetClosestHitProgram(0, CHPerspective);
	OptiXMaterial->SetClosestHitProgram(1, CHIterative);

	OptiXMaterial->SetFloat("importance_cutoff", 1e-2f);
	OptiXMaterial->SetFloat3D("cutoff_color", 0.035f, 0.102f, 0.169f);
	OptiXMaterial->SetFloat("fresnel_exponent", 3.0f);
	OptiXMaterial->SetFloat("fresnel_minimum", 0.1f);
	OptiXMaterial->SetFloat("fresnel_maximum", 1.0f);
	OptiXMaterial->SetFloat("refraction_index", 1.4f);

	OptiXMaterial->SetFloat3D("refraction_color", 1.0f, 1.0f, 1.0f);
	OptiXMaterial->SetFloat3D("reflection_color", 1.0f, 1.0f, 1.0f);

	OptiXMaterial->SetInt("refraction_maxdepth", 10);
	OptiXMaterial->SetInt("reflection_maxdepth", 5);

	OptiXMaterial->SetFloat3D("extinction_constant", FMath::Loge(0.83f), FMath::Loge(0.83f), FMath::Loge(0.83f));

	OptiXMaterial->SetInt("lens_id", OptiXCubemapId);

}

void UOptiXLensComponent::InitOptiXGroups()
{
	OptiXGeometryInstance = OptiXContext->CreateGeometryInstance(OptiXGeometry, OptiXMaterial);

	OptiXGeometryInstance->SetFloat3D("center", 0.0f, 0.0f, 0.0f);

	OptiXGeometryInstance->SetFloat3DVector("orientation", {0, 0, -1}); // TODO Why this vector?!?!


	SetRadius1(Radius1);
	SetRadius2(Radius2);
	SetLensRadius(LensRadius);
	SetThickness(LensThickness);
	SetLensType1(LensType1);
	SetLensType2(LensType2);
	SetWavelength(CurrentWavelength); // min value

	OptiXTransform = OptiXContext->CreateTransform();

	FMatrix WorldTransform = GetComponentToWorld().ToMatrixNoScale();
	OptiXTransform->SetMatrix(WorldTransform);

	OptiXGeometryGroup = OptiXContext->CreateGeometryGroup();
	OptiXGeometryGroup->AddChild(OptiXGeometryInstance);

	OptiXAcceleration = OptiXContext->CreateAcceleration("NoAccel"); // Seems to be faster for now
	//OptiXAcceleration->SetProperty("refit", "1");

	OptiXGeometryGroup->SetAcceleration(OptiXAcceleration);

	OptiXTransform->SetChild(OptiXGeometryGroup);


	OptiXContext->GetGroup("top_object")->AddChild(OptiXTransform);
	MarkDirty();
}

void UOptiXLensComponent::InitCubemap(FRHICommandListImmediate & RHICmdList)
{
	UE_LOG(LogTemp, Display, TEXT("Init Cubemap"));

	//OptiXContext->SetBuffer("skyboxBuffer", CubemapBuffer);

	CubemapSampler = OptiXContext->CreateTextureSampler();
	//CubemapSampler->AddToRoot();
	CubemapSampler->SetWrapMode(0, RT_WRAP_CLAMP_TO_EDGE);
	CubemapSampler->SetWrapMode(1, RT_WRAP_CLAMP_TO_EDGE);
	CubemapSampler->SetWrapMode(2, RT_WRAP_CLAMP_TO_EDGE);
	CubemapSampler->SetIndexingMode(RT_TEXTURE_INDEX_NORMALIZED_COORDINATES);
	CubemapSampler->SetReadMode(RT_TEXTURE_READ_NORMALIZED_FLOAT);
	CubemapSampler->SetMaxAnisotropy(1.0f);
	CubemapSampler->SetMipLevelCount(1u);
	CubemapSampler->SetArraySize(1u);


	CubemapBuffer = OptiXContext->CreateCubemapBuffer(1024, 1024);
	//CubemapBuffer->AddToRoot();

	CubemapSampler->SetBufferWithTextureIndexAndMiplevel(0u, 0u, CubemapBuffer);
	CubemapSampler->SetFilteringModes(RT_FILTER_LINEAR, RT_FILTER_LINEAR, RT_FILTER_NONE);
	

	UpdateCubemap(RHICmdList);


	// Writes the variable into the input buffer
	FOptiXModule::Get().GetOptiXContextManager()->AddCubemapToBuffer(OptiXCubemapId, CubemapSampler->GetId());
	UE_LOG(LogTemp, Display, TEXT("Finished Init Cubemap"));

}

void UOptiXLensComponent::UpdateCubemap(FRHICommandListImmediate & RHICmdList)
{

	UE_LOG(LogTemp, Display, TEXT("Updating Cubemap"));


	int32 X = 1024; // todo hardcoded
	int32 Y = X;

	TArray<TArray<FColor>> SurfaceDataCube;
	SurfaceDataCube.SetNumZeroed(6);

	//TArray<FLinearColor> SD;

	optix::uchar4* BufferData = static_cast<optix::uchar4*>(CubemapBuffer->MapNative());

	FTextureRenderTargetCubeResource* RenderTargetCube = static_cast<FTextureRenderTargetCubeResource*>(CubeRenderTarget->GetRenderTargetResource());

	FIntRect InRectCube = FIntRect(0, 0, RenderTargetCube->GetSizeXY().X, RenderTargetCube->GetSizeXY().Y);
	FReadSurfaceDataFlags FlagsCube0(RCM_UNorm, CubeFace_PosX);
	FReadSurfaceDataFlags FlagsCube1(RCM_UNorm, CubeFace_NegX);
	FReadSurfaceDataFlags FlagsCube2(RCM_UNorm, CubeFace_PosY);
	FReadSurfaceDataFlags FlagsCube3(RCM_UNorm, CubeFace_NegY);
	FReadSurfaceDataFlags FlagsCube4(RCM_UNorm, CubeFace_PosZ);
	FReadSurfaceDataFlags FlagsCube5(RCM_UNorm, CubeFace_NegZ);

	RHICmdList.ReadSurfaceData(RenderTargetCube->GetTextureRHI(), InRectCube, SurfaceDataCube[0], FlagsCube0);
	RHICmdList.ReadSurfaceData(RenderTargetCube->GetTextureRHI(), InRectCube, SurfaceDataCube[1], FlagsCube1);
	RHICmdList.ReadSurfaceData(RenderTargetCube->GetTextureRHI(), InRectCube, SurfaceDataCube[2], FlagsCube2);
	RHICmdList.ReadSurfaceData(RenderTargetCube->GetTextureRHI(), InRectCube, SurfaceDataCube[3], FlagsCube3);
	RHICmdList.ReadSurfaceData(RenderTargetCube->GetTextureRHI(), InRectCube, SurfaceDataCube[4], FlagsCube4);
	RHICmdList.ReadSurfaceData(RenderTargetCube->GetTextureRHI(), InRectCube, SurfaceDataCube[5], FlagsCube5);

	uint32 MemSize = (X * Y * sizeof(FColor));
	FMemory::Memcpy(BufferData, SurfaceDataCube[0].GetData(), MemSize); // front
	FMemory::Memcpy(BufferData + X * Y * 1, SurfaceDataCube[1].GetData(), MemSize); // back
	FMemory::Memcpy(BufferData + X * Y * 2, SurfaceDataCube[2].GetData(), MemSize); // 
	FMemory::Memcpy(BufferData + X * Y * 3, SurfaceDataCube[3].GetData(), MemSize); // 
	FMemory::Memcpy(BufferData + X * Y * 4, SurfaceDataCube[4].GetData(), MemSize); // 
	FMemory::Memcpy(BufferData + X * Y * 5, SurfaceDataCube[5].GetData(), MemSize); //

	CubemapBuffer->Unmap();

	UE_LOG(LogTemp, Display, TEXT("Finished Updating Cubemap"));


}


void UOptiXLensComponent::CleanOptiXComponent()
{
	if(OptiXContext != NULL && OptiXContext->GetGroup("top_object") != NULL)
		OptiXContext->GetGroup("top_object")->RemoveChild(OptiXTransform);
	OptiXTransform = nullptr;

	Super::CleanOptiXComponent();
	OptiXGeometryGroup = nullptr;

}

void UOptiXLensComponent::InitFromData(const FLensData& Data)
{
	SetLensRadius(Data.LensRadius * 10);
	SetRadius1(Data.Radius1 * 10);
	SetRadius2(Data.Radius2 * 10);
	SetThickness(Data.Thickness);
	SetLensType1(Data.LensTypeSide1);
	SetLensType2(Data.LensTypeSide2);
	SetGlassType(Data.GlassType);
}

void UOptiXLensComponent::SetThickness(float Thickness)
{
	UE_LOG(LogTemp, Display, TEXT("Setting Thickness: %f"), Thickness);
	LensThickness = Thickness;
	QueueOptiXContextUpdate();
	if(IsInGameThread())
		OnLensThicknessChanged.Broadcast(LensThickness);
}

float UOptiXLensComponent::GetThickness() const
{
	// No silly conversions...
	return LensThickness;
}

void UOptiXLensComponent::SetRadius1(float Radius)
{
	UE_LOG(LogTemp, Display, TEXT("Setting Radius 1: %f"), Radius);
	Radius1 = Radius;
	QueueOptiXContextUpdate();
}

float UOptiXLensComponent::GetRadius1() const
{
	return Radius1;
}

void UOptiXLensComponent::SetRadius2(float Radius)
{

	UE_LOG(LogTemp, Display, TEXT("Setting Radius 2: %f"), Radius);
	Radius2 = Radius;
	QueueOptiXContextUpdate();
}

float UOptiXLensComponent::GetRadius2() const
{
	return Radius2;
}

void UOptiXLensComponent::SetLensRadius(float Radius)
{
	UE_LOG(LogTemp, Display, TEXT("Setting Lens Radius: %f"), Radius);
	LensRadius = Radius;
	QueueOptiXContextUpdate();
	if (IsInGameThread())
		OnLensRadiusChanged.Broadcast(Radius);
}

float UOptiXLensComponent::GetLensRadius() const
{
	return LensRadius;
}

void UOptiXLensComponent::SetLensType1(ELensSideType Type)
{
	UE_LOG(LogTemp, Display, TEXT("Setting Lens Side 1 Type: %i"), static_cast<int>(Type));

	LensType1 = Type;
	// Recalculate Thickness and set new half cylinder length
	//SetThickness(LensThickness);
	QueueOptiXContextUpdate();
}

ELensSideType UOptiXLensComponent::GetLensType1() const
{
	return LensType1;
}

void UOptiXLensComponent::SetLensType2(ELensSideType Type)
{
	UE_LOG(LogTemp, Display, TEXT("Setting Lens Side 2 Type: %i"), static_cast<int>(Type));

	LensType2 = Type;
	// Recalculate Thickness and set new half cylinder length
	//SetThickness(LensThickness);
	QueueOptiXContextUpdate();
}

ELensSideType UOptiXLensComponent::GetLensType2() const
{
	return LensType2;
}

void UOptiXLensComponent::SetGlassType(FString Type)
{
	UE_LOG(LogTemp, Display, TEXT("Setting Glass Type: %s"), *Type);

	GlassType = Type;
	SetWavelength(CurrentWavelength); // ???
	QueueOptiXContextUpdate();
}

FString UOptiXLensComponent::GetGlassType() const
{
	return GlassType;
}

void UOptiXLensComponent::SetWavelength(float WL)
{
	UE_LOG(LogTemp, Display, TEXT("Setting new WL in lens: %s"), *GetName());
	CurrentWavelength = WL;
	QueueOptiXContextUpdate();
}

float UOptiXLensComponent::GetWavelength() const
{
	return CurrentWavelength;
}

void UOptiXLensComponent::MarkDirty()
{

	UE_LOG(LogTemp, Display, TEXT("Marking Dirty"));


	if (OptiXGeometry != nullptr)
	{
		OptiXGeometry->MarkDirty();
	}
	if (OptiXAcceleration != nullptr)
	{
		OptiXAcceleration->MarkDirty();
	}

	//manager_->TopAccelerationMarkDirty();
	UOptiXAcceleration* TopAccel = OptiXContext->GetGroup("top_object")->GetAcceleration();
	if (TopAccel != nullptr)
	{
		TopAccel->MarkDirty(); // This should never be null, but check anyway
	}

	//RecalculateBoundingBox(); // TODO

}


float UOptiXLensComponent::GetCylinderLength(float Thickness) const
{
	// Halfsphere thickness
	float HalfThickness1;
	float HalfThickness2;

	// Side 1
	if (LensType1 == ELensSideType::PLANE)
	{
		HalfThickness1 = 0;
	}
	else
	{
		HalfThickness1 = Radius1 - FMath::Sqrt(-LensRadius * LensRadius + Radius1 * Radius1);
		if (LensType1 == ELensSideType::CONCAVE)
		{
			HalfThickness1 *= -1;
		}
	}

	// Side 2
	if (LensType2 == ELensSideType::PLANE)
	{
		HalfThickness2 = 0;
	}
	else
	{
		HalfThickness2 = Radius2 - FMath::Sqrt(-LensRadius * LensRadius + Radius2 * Radius2);
		if (LensType2 == ELensSideType::CONCAVE)
		{
			HalfThickness2 *= -1;
		}
	}
	return Thickness - HalfThickness1 - HalfThickness2;
}


void UOptiXLensComponent::RecalculateBoundingBox()
{

	// Do we even need this? Seems like it was just used for phoenix stuff TODO
	return;

	// TODO Shouldn't have to call the getter here - this should just be the actor rotation transform?
	/*FVector Orientation = OptiXGeometry->GetFloat3D("orientation");
	float HalfThickness1;	
	LensType1 == ELensSideType::CONVEX ? HalfThickness1 = Radius1 - FMath::Sqrt(-LensRadius * LensRadius + Radius1 * Radius1) : HalfThickness1 = 0;

	float HalfThickness2;
	LensType2 == ELensSideType::CONVEX ? HalfThickness2 = Radius2 - FMath::Sqrt(-LensRadius * LensRadius + Radius2 * Radius2) : HalfThickness2 = 0;
*/
	// TODO

}

TArray<FString> UOptiXLensComponent::GetGlassDefinitionNames()
{
	TArray<FString> Names;

	for (const TPair<FString, FGlassDefinition>& Pair : FOptiXModule::Get().GetGlassDefinitions())
	{
		Names.Add(Pair.Key);
	}

	return Names;
}

void UOptiXLensComponent::OnWavelengthChangedEvent(float WL)
{
	SetWavelength(WL);
}