// Project Lab - NHTV Igad

#include "UnrealProject.h"
#include "MiniMapVolume.h"
#include "MiniMapIconWidget.h"
#include "MiniMapVisionCircle.h"
#include "NetworkPlayer.h"
#include "BossBase.h"
#include "NPCBase.h"
#include "KOTHBossSpawner.h"
#include "KOTHMinionSpawner.h"
#include "NetworkCharacter.h"
#include "DefaultGameState.h"
#include "DefaultPlayerState.h"
#include "DefaultPlayerController.h"
#include "MiniMapWidget.h"
#include "WidgetLayoutLibrary.h"

#if PLATFORM_SPECIFIC_WIN == 0 
#include "HeatMapMetrics.h"
#endif


float AngleTowards(const FVector2D& from, const FVector2D& to)
{
	const float angle_rad = FMath::Atan2(from.X - to.X, to.Y - from.Y) + PI;
	return FMath::RadiansToDegrees(angle_rad);
}

UMiniMapWidget::UMiniMapWidget(const FObjectInitializer& init) : Super(init)
{
	viewRadius = 2000;
}

void UMiniMapWidget::NativeConstruct()
{
	Super::NativeConstruct();

	// Find the minimap volume
	ADefaultGameState* const gameState = Cast<ADefaultGameState>(GetWorld()->GetGameState());
	if(IsValid(gameState))
	{
		for (TActorIterator<AMiniMapVolume> iter(GetWorld()); iter; ++iter)
		{
			AMiniMapVolume* const volume = *iter;
			volume->SetActorRotation(FRotator(0, 0, 0));

			const FVector pos = volume->GetActorLocation();
			const FVector area = volume->area->GetScaledBoxExtent();
			m_size = FMath::Max(area.X, area.Y);
			m_min.X = -m_size + pos.X;
			m_min.Y = -m_size + pos.Y;
			m_max.X = m_size + pos.X;
			m_max.Y = m_size + pos.Y;
			m_size *= 2;

			m_viewRadius = (viewRadius / (area.X * 2));

			gameState->minimapQuadtree.Resize(MiniMap::Rect(FVector2D(m_min), FVector2D(m_size, m_size)));

			return;
		}
	}

	JERROR("Failed to locate Mini Map volume");
}

void UMiniMapWidget::NativeTick(const FGeometry& geometry, float deltaTime)
{
	Super::NativeTick(geometry, deltaTime);

	m_arrowAnim = FMath::Fmod(m_arrowAnim + deltaTime, 1.0f);

	// Reset the state of all current widgets
	{
		// Set widgets hidden
		for (int32 i = 0; i < m_miniMapIconWidgetPool.Num(); i++)
		{
			m_miniMapIconWidgetPool[i]->SetVisibility(ESlateVisibility::Hidden);
			// Reset rotations
			FWidgetTransform trans = m_miniMapIconWidgetPool[i]->image->RenderTransform;
			trans.Angle = 0;
			m_miniMapIconWidgetPool[i]->image->SetRenderTransform(trans);
		}

		// Set existing arrows hidden
		for (int32 i = 0; i < m_arrowMapIconWidgetPool.Num(); i++)
			m_arrowMapIconWidgetPool[i]->SetVisibility(ESlateVisibility::Hidden);

		// Allocate the neccesairy vision circles
		for (int32 i = 0; i < m_viewCircleWidgetPool.Num(); i++)
			m_viewCircleWidgetPool[i]->SetVisibility(ESlateVisibility::Hidden);
	}

	// Begin the rendering
	UWorld* const world = GetWorld();
	if (!IsValid(world)) return;
	ADefaultGameState* const gameState = Cast<ADefaultGameState>(world->GetGameState());
	if (!IsValid(gameState))
		return;

	// Get the local team
	m_localTeam = -1;
	AController* controller = world->GetGameInstance()->GetFirstLocalPlayerController();
	ACharacterBase* localPlayer = nullptr;
	if (IsValid(controller))
	{
		ADefaultPlayerController* const playerController = Cast<ADefaultPlayerController>(controller);
		localPlayer = Cast<ACharacterBase>(playerController->GetPawn());
		if (playerController->PlayerState)
			m_localTeam = Cast<ADefaultPlayerState>(playerController->PlayerState)->GetTeam();
	}

	// Failed to fetch local team
	if (m_localTeam == -1)
		return;
	
	gameState->minimapQuadtree.Update();
	
	map<int32, unordered_set<ACharacterBase*>> drawActors;
	unordered_set<ACharacterBase*>& friendlyPlayerSet = drawActors.emplace(m_localTeam, unordered_set<ACharacterBase*>()).first->second;

	// Fetch the friendly viewpoints
	for (TActorIterator<ACharacterBase> iter(world); iter; ++iter)
	{
		ACharacterBase* const character = *iter;
		if (IsValid(character))
		{
			if (character->GetTeam() == m_localTeam)
				friendlyPlayerSet.emplace(character);
		}
	}
	
	// Fetch all the visible units around the friendly viewpoints
	for (auto iter = friendlyPlayerSet.begin(); iter != friendlyPlayerSet.end(); iter++)
	{
		ACharacterBase* const character = (*iter);
		FVector2D position = FVector2D(character->GetActorLocation().X, character->GetActorLocation().Y);
		if (gameState->minimapQuadtree.CircleOverlap(position, character->visionRadius, m_handleBuffer))
		{
			for (int32 i = 0; i < m_handleBuffer.Num(); i++)
			{
				ACharacterBase* const visibleCharacter = m_handleBuffer[i]->character;
				if(IsValid(visibleCharacter))
					drawActors[visibleCharacter->GetTeam()].emplace(visibleCharacter);
			}
		}
	}

	// Fetch the minion camps
	TArray<AKOTHSpawnerBase*> minionCamps;
	AKOTHBossSpawner* bossCamp = nullptr;
	FVector2D bossCampPos;
	for (TActorIterator<AKOTHSpawnerBase> iter(world); iter; ++iter)
	{
		AKOTHSpawnerBase* camp = *iter;
		if (camp->IsA<AKOTHBossSpawner>())
		{
			bossCamp = Cast<AKOTHBossSpawner>(camp);
			const FVector2D charPos = FVector2D(camp->GetActorLocation().X, camp->GetActorLocation().Y);
			bossCampPos = ConvertWorldToMinimap(charPos);
		}
		minionCamps.Add(camp);
	}

	// Draw the minion camps
	m_widgetIndex = 0;
	m_arrowIndex = 0;
	m_circleIndex = 0;
	for (int32 i = 0; i < minionCamps.Num(); i++)
	{
		UMiniMapIconWidget* const widget = m_AllocateWidget();
		UCanvasPanelSlot* const slot = Cast<UCanvasPanelSlot>(widget->Slot);
		widget->SetVisibility(ESlateVisibility::Visible);

		AKOTHSpawnerBase* const camp = minionCamps[i];
		const FVector2D charPos = FVector2D(camp->GetActorLocation().X, camp->GetActorLocation().Y);
		FVector2D relativePos = ConvertWorldToMinimap(charPos);

		// Set sprite position
		FVector2D spriteSize = geometry.GetLocalSize() * 0.07f;

		slot->SetPosition(geometry.GetLocalSize() * relativePos - spriteSize * 0.5f);
		slot->SetSize(spriteSize);
		SetObjectIcon(campIcon, widget);

		FLinearColor color = enemyColor;
		if (camp->team >= NPCTeam::Team1) // NPC Controlled
			color = playerColor;
		else if (int32(camp->team) == int32(m_localTeam)) // Friendly
			color = allyColor;
		else if (int32(camp->team) == 0) // Contested
			color = neutralColor;
		widget->image->Brush.TintColor = color;

		// Draw arrows
		if (bossCamp && int32(camp->team) > 0 && camp->team < NPCTeam::Team1 && camp != bossCamp)
		{
			const int32 arrowCount = int32(FVector2D::Distance(relativePos, bossCampPos) * 25);
			if (arrowCount > 0)
			{
				const float step = 1.0f / float(arrowCount);
				for (int32 i = 0; i < arrowCount; i++)
				{
					UMiniMapIconWidget* arrow = m_AllocateArrow();
					arrow->SetVisibility(ESlateVisibility::Visible);
					UCanvasPanelSlot* const slot = Cast<UCanvasPanelSlot>(arrow->Slot);

					// Lerp towards the target
					color.A = arrowCount > 1 ? (i == 0 ? m_arrowAnim : i == arrowCount - 1 ? (1.f - m_arrowAnim) : 1) : 1;
					const FVector2D setPos = FMath::Lerp(relativePos, bossCampPos, step * float(i) + step * m_arrowAnim);
					FWidgetTransform trans = arrow->image->RenderTransform;
					trans.Angle = AngleTowards(setPos, bossCampPos);
					arrow->image->SetRenderTransform(trans);
					arrow->image->Brush.TintColor = color;

					const FVector2D spriteSize = geometry.GetLocalSize() * 0.04f;
					slot->SetSize(spriteSize);
					slot->SetPosition(geometry.GetLocalSize() * setPos - spriteSize * 0.5f);
				}
			}
		}
	}

	// Generate draw order (Draw local team last)
	TArray<int32> teamDrawOrder;
	for (auto iter = drawActors.rbegin(); iter != drawActors.rend(); iter++)
	{
		if (iter->first == m_localTeam)
			continue;
		teamDrawOrder.Add(iter->first);
	}
	teamDrawOrder.Add(m_localTeam);

	// Draw the actual characters to the minimap
	int32 circleIndex = 0;
	for (size_t i = 0; i < teamDrawOrder.Num(); i++)
	{
		const int32 team = teamDrawOrder[i];
		auto& set = drawActors[team];
		for (auto charIter = set.begin(); charIter != set.end(); charIter++)
		{
			ACharacterBase* const character = *charIter;
			if (character == localPlayer)
				continue;

			UMiniMapIconWidget* const widget = m_AllocateWidget();
			m_DrawCreature(geometry, widget, character);
		}
	}

	// Draw the local player last
	if (localPlayer)
		m_DrawCreature(geometry, m_AllocateWidget(), localPlayer);
}

void UMiniMapWidget::m_DrawCreature(const FGeometry& geometry, UMiniMapIconWidget* widget, class ACharacterBase* character)
{
	widget->SetVisibility(ESlateVisibility::Visible);
	UCanvasPanelSlot* const slot = Cast<UCanvasPanelSlot>(widget->Slot);

	const int32 actorTeam = character->GetTeam();
	const FVector2D charPos = FVector2D(character->GetActorLocation().X, character->GetActorLocation().Y);
	FVector2D relativePos = ConvertWorldToMinimap(charPos);

	// Rotation
	const float drawPlayerAngle = character->GetActorRotation().Yaw;

	// Set sprite position
	FVector2D spriteSize = geometry.GetLocalSize() * 0.07f;

	slot->SetPosition(geometry.GetLocalSize() * relativePos - spriteSize * 0.5f);
	slot->SetSize(spriteSize);

	FWidgetTransform trans = widget->image->RenderTransform;
	trans.Angle = 0;

	// Set icon based on the object type
	if (character->IsA<ABossBase>())
		SetObjectIcon(bossIcon, widget);
	else if (character->IsA<ANPCBase>())
		SetObjectIcon(creatureIcon, widget);
	else
	{
		trans.Angle = drawPlayerAngle;
		SetObjectIcon(playerIcon, widget);
	}
	widget->image->SetRenderTransform(trans);

	// Set the color
	if (!(character->IsA<ANetworkPlayer>() && character->IsLocallyControlled()))
	{
		if (actorTeam != m_localTeam)
		{
			// Neutral or enemy?
			if (actorTeam == 0)
				widget->image->Brush.TintColor = neutralColor;
			else
				widget->image->Brush.TintColor = enemyColor;
		}
		else
			widget->image->Brush.TintColor = allyColor;
	}
	else
		widget->image->Brush.TintColor = playerColor;

	// Update the view circle
	if (actorTeam == m_localTeam)
	{
		UMiniMapVisionCircle* const circle = m_AllocateVisionCircle();
		circle->SetVisibility(ESlateVisibility::Visible);

		UCanvasPanelSlot* const circleSlot = Cast<UCanvasPanelSlot>(circle->Slot);
		if (IsValid(circleSlot))
		{
			const float diameter = (character->visionRadius * 2) / m_size;
			FVector2D size = geometry.GetLocalSize() * diameter;
			size = FVector2D(FMath::RoundToFloat(size.X), FMath::RoundToFloat(size.Y));
			FVector2D pos = geometry.GetLocalSize() * relativePos - size * 0.5f;
			pos = FVector2D(FMath::RoundToFloat(pos.X), FMath::RoundToFloat(pos.Y));

			circleSlot->SetPosition(pos);
			circleSlot->SetSize(size);
			circle->SetUV(pos / geometry.GetLocalSize(), size / geometry.GetLocalSize());
		}
	}
}
UMiniMapIconWidget* UMiniMapWidget::m_AllocateWidget()
{
	UMiniMapIconWidget* widget = nullptr;
	if (m_widgetIndex >= m_miniMapIconWidgetPool.Num())
	{
		widget = CreateWidget<UMiniMapIconWidget>(GetWorld(), iconWidget);
		iconLayer->AddChild(widget);
		m_miniMapIconWidgetPool.Add(widget);
	}
	else
		widget = m_miniMapIconWidgetPool[m_widgetIndex];

	m_widgetIndex++;
	return widget;
}
UMiniMapIconWidget* UMiniMapWidget::m_AllocateArrow()
{
	UMiniMapIconWidget* arrow = nullptr;
	if (m_arrowIndex >= m_arrowMapIconWidgetPool.Num())
	{
		arrow = CreateWidget<UMiniMapIconWidget>(GetWorld(), iconWidget);
		arrowLayer->AddChild(arrow);
		m_arrowMapIconWidgetPool.Add(arrow);
		SetObjectIcon(arrowIcon, arrow);
	}
	else
		arrow = m_arrowMapIconWidgetPool[m_arrowIndex];

	m_arrowIndex++;
	return arrow;
}
UMiniMapVisionCircle* UMiniMapWidget::m_AllocateVisionCircle()
{
	UMiniMapVisionCircle* circle = nullptr;
	if (m_circleIndex >= m_viewCircleWidgetPool.Num())
	{
		circle = CreateWidget<UMiniMapVisionCircle>(GetWorld(), visionCircleWidget);
		visionLayer->AddChild(circle);
		m_viewCircleWidgetPool.Add(circle);
		circle->SetTexture(backgroundTexture);
	}
	else
		circle = m_viewCircleWidgetPool[m_circleIndex];

	m_circleIndex++;
	return circle;
}

FVector2D UMiniMapWidget::ConvertWorldToMinimap(const FVector2D& pos)
{
	const FVector2D scale = FVector2D(m_max.X - m_min.X, m_max.Y - m_min.Y);
	FVector2D relativePos = (pos - m_min) / scale;
	FVector2D returnPos = FVector2D(relativePos.Y, relativePos.X);
	returnPos.Y = 1.0f - returnPos.Y;
	return returnPos;
}

void UMiniMapWidget::SetObjectIcon_Implementation(UTexture2D* texture, UMiniMapIconWidget* widget)
{
	// No implementation
}

void UMiniMapWidget::SetMinimapInfo_Implementation(FVector2D player0, FVector2D player1, float radius)
{
	// No implementation
}