// Copyright Pacificao. All Rights Reserved.

#include "AgrarianBuildingPlacementComponent.h"
#include "AgrarianInventoryComponent.h"
#include "DrawDebugHelpers.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/Controller.h"
#include "Engine/World.h"

UAgrarianBuildingPlacementComponent::UAgrarianBuildingPlacementComponent()
{
	PrimaryComponentTick.bCanEverTick = true;
	SetIsReplicatedByDefault(true);
}

void UAgrarianBuildingPlacementComponent::SetActiveBuildable(TSubclassOf<AActor> BuildClass, const TArray<FAgrarianItemStack>& Cost)
{
	ActiveBuildClass = BuildClass;
	PlacementCost = Cost;
}

bool UAgrarianBuildingPlacementComponent::GetPlacementPreview(FTransform& OutTransform, FText& FailureReason) const
{
	const APawn* OwnerPawn = Cast<APawn>(GetOwner());
	if (!OwnerPawn)
	{
		FailureReason = FText::FromString(TEXT("Only pawns can place buildables."));
		return false;
	}

	FVector ViewLocation;
	FRotator ViewRotation;
	if (OwnerPawn->GetController())
	{
		OwnerPawn->GetController()->GetPlayerViewPoint(ViewLocation, ViewRotation);
	}
	else
	{
		ViewLocation = OwnerPawn->GetActorLocation();
		ViewRotation = OwnerPawn->GetActorRotation();
	}

	const FVector TraceEnd = ViewLocation + ViewRotation.Vector() * PlacementDistance;
	FHitResult Hit;
	FCollisionQueryParams Params(SCENE_QUERY_STAT(AgrarianBuildPlacementTrace), false, OwnerPawn);

	FVector PlacementLocation = TraceEnd;
	if (GetWorld() && GetWorld()->LineTraceSingleByChannel(Hit, ViewLocation, TraceEnd, ECC_Visibility, Params))
	{
		PlacementLocation = Hit.ImpactPoint + Hit.ImpactNormal * SurfaceOffset;
	}

	PlacementLocation = SnapLocation(PlacementLocation);
	const FRotator PlacementRotation(0.0f, OwnerPawn->GetActorRotation().Yaw, 0.0f);
	OutTransform = FTransform(PlacementRotation, PlacementLocation);
	return true;
}

bool UAgrarianBuildingPlacementComponent::GetPlacementPreviewState(FTransform& OutTransform, FText& FailureReason) const
{
	if (!GetPlacementPreview(OutTransform, FailureReason))
	{
		return false;
	}

	return CanPlaceAtTransform(ActiveBuildClass, OutTransform, FailureReason);
}

bool UAgrarianBuildingPlacementComponent::CanPlaceAtTransform(TSubclassOf<AActor> BuildClass, const FTransform& PlacementTransform, FText& FailureReason) const
{
	if (!BuildClass)
	{
		FailureReason = FText::FromString(TEXT("No buildable is selected."));
		return false;
	}

	const AActor* OwnerActor = GetOwner();
	if (!OwnerActor)
	{
		FailureReason = FText::FromString(TEXT("No owner is available."));
		return false;
	}

	if (FVector::DistSquared(OwnerActor->GetActorLocation(), PlacementTransform.GetLocation()) > FMath::Square(PlacementDistance + 150.0f))
	{
		FailureReason = FText::FromString(TEXT("Placement is too far away."));
		return false;
	}

	if (!GetWorld())
	{
		FailureReason = FText::FromString(TEXT("No world is available."));
		return false;
	}

	FCollisionQueryParams Params(SCENE_QUERY_STAT(AgrarianBuildPlacementProbe), false, OwnerActor);
	const bool bBlocked = GetWorld()->OverlapBlockingTestByChannel(
		PlacementTransform.GetLocation() + FVector(0.0f, 0.0f, PlacementProbeRadius),
		PlacementTransform.GetRotation(),
		ECC_WorldDynamic,
		FCollisionShape::MakeSphere(PlacementProbeRadius),
		Params);

	if (bBlocked)
	{
		FailureReason = FText::FromString(TEXT("Placement area is blocked."));
		return false;
	}

	return true;
}

bool UAgrarianBuildingPlacementComponent::PlaceActiveBuildable()
{
	FTransform PlacementTransform;
	FText FailureReason;
	if (!GetPlacementPreview(PlacementTransform, FailureReason))
	{
		FailPlacement(FailureReason);
		return false;
	}

	if (!GetOwner())
	{
		return false;
	}

	if (!GetOwner()->HasAuthority())
	{
		ServerPlaceBuildable(ActiveBuildClass, PlacementTransform);
		return true;
	}

	ServerPlaceBuildable_Implementation(ActiveBuildClass, PlacementTransform);
	return true;
}

void UAgrarianBuildingPlacementComponent::ServerPlaceBuildable_Implementation(TSubclassOf<AActor> BuildClass, FTransform PlacementTransform)
{
	FText FailureReason;
	if (!CanPlaceAtTransform(BuildClass, PlacementTransform, FailureReason) || !HasPlacementCost(FailureReason))
	{
		FailPlacement(FailureReason);
		return;
	}

	FActorSpawnParameters SpawnParams;
	SpawnParams.Owner = GetOwner();
	SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;

	AActor* PlacedActor = GetWorld()->SpawnActor<AActor>(BuildClass, PlacementTransform, SpawnParams);
	if (!PlacedActor)
	{
		FailPlacement(FText::FromString(TEXT("Buildable could not be placed.")));
		return;
	}

	ConsumePlacementCost();
	OnBuildPlaced.Broadcast(PlacedActor);
}

void UAgrarianBuildingPlacementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (!bShowGhostPreview || !ActiveBuildClass || !GetWorld())
	{
		return;
	}

	FTransform PreviewTransform;
	FText FailureReason;
	const bool bCanPlace = GetPlacementPreviewState(PreviewTransform, FailureReason);
	const FColor PreviewColor = bCanPlace ? ValidGhostPreviewColor : InvalidGhostPreviewColor;
	const FVector PreviewExtent(
		FMath::Max(PlacementProbeRadius, 25.0f),
		FMath::Max(PlacementProbeRadius, 25.0f),
		FMath::Max(PlacementProbeRadius * 0.5f, 25.0f));

	DrawDebugBox(
		GetWorld(),
		PreviewTransform.GetLocation() + FVector(0.0f, 0.0f, PreviewExtent.Z),
		PreviewExtent,
		PreviewTransform.GetRotation(),
		PreviewColor,
		false,
		GhostPreviewLifetimeSeconds,
		0,
		GhostPreviewLineThickness);

	OnBuildPreviewUpdated.Broadcast(bCanPlace, PreviewTransform, FailureReason);
}

bool UAgrarianBuildingPlacementComponent::HasPlacementCost(FText& FailureReason) const
{
	const UAgrarianInventoryComponent* Inventory = GetOwner() ? GetOwner()->FindComponentByClass<UAgrarianInventoryComponent>() : nullptr;
	if (!Inventory)
	{
		FailureReason = FText::FromString(TEXT("No inventory is available."));
		return false;
	}

	for (const FAgrarianItemStack& Cost : PlacementCost)
	{
		if (!Inventory->HasItem(Cost.ItemId, Cost.Quantity))
		{
			FailureReason = FText::Format(
				FText::FromString(TEXT("Missing build material: {0}")),
				Cost.DisplayName.IsEmpty() ? FText::FromName(Cost.ItemId) : Cost.DisplayName);
			return false;
		}
	}

	return true;
}

void UAgrarianBuildingPlacementComponent::ConsumePlacementCost()
{
	UAgrarianInventoryComponent* Inventory = GetOwner() ? GetOwner()->FindComponentByClass<UAgrarianInventoryComponent>() : nullptr;
	if (!Inventory)
	{
		return;
	}

	for (const FAgrarianItemStack& Cost : PlacementCost)
	{
		Inventory->RemoveItem(Cost.ItemId, Cost.Quantity);
	}
}

void UAgrarianBuildingPlacementComponent::FailPlacement(const FText& Reason)
{
	OnBuildPlacementFailed.Broadcast(Reason);
}

FVector UAgrarianBuildingPlacementComponent::SnapLocation(const FVector& Location) const
{
	if (!bSnapToGrid || GridSize <= 0.0f)
	{
		return Location;
	}

	return FVector(
		FMath::GridSnap(Location.X, GridSize),
		FMath::GridSnap(Location.Y, GridSize),
		Location.Z);
}
