// Copyright Epic Games, Inc. All Rights Reserved.

#include "CommonPlayerInputKey.h"

#include "CommonInputSubsystem.h"
#include "CommonLocalPlayer.h"
#include "CommonPlayerController.h"
#include "Components/SlateWrapperTypes.h"
#include "Containers/UnrealString.h"
#include "Delegates/Delegate.h"
#include "Engine/World.h"
#include "Fonts/FontMeasure.h"
#include "Framework/Application/SlateApplication.h"
#include "Internationalization/Internationalization.h"
#include "Layout/Geometry.h"
#include "Logging/LogCategory.h"
#include "Logging/LogMacros.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Materials/MaterialInterface.h"
#include "Math/Color.h"
#include "Math/UnrealMathSSE.h"
#include "Misc/AssertionMacros.h"
#include "Rendering/DrawElements.h"
#include "Rendering/RenderingCommon.h"
#include "Rendering/SlateRenderer.h"
#include "Styling/SlateBrush.h"
#include "Styling/WidgetStyle.h"
#include "Templates/Casts.h"
#include "Templates/SharedPointer.h"
#include "TimerManager.h"
#include "Trace/Detail/Channel.h"
#include "UObject/UnrealNames.h"
#include "Widgets/InvalidateWidgetReason.h"

class FPaintArgs;
class FSlateRect;

#define LOCTEXT_NAMESPACE "CommonKeybindWidget"

DECLARE_LOG_CATEGORY_EXTERN(LogCommonPlayerInput, Log, All);
DEFINE_LOG_CATEGORY(LogCommonPlayerInput);

struct FSlateDrawUtil
{
	static void DrawBrushCenterFit(
		FSlateWindowElementList& ElementList,
		uint32 InLayer,
		const FGeometry& InAllottedGeometry,
		const FSlateBrush* InBrush,
		const FLinearColor& InTint = FLinearColor::White)
	{
		DrawBrushCenterFitWithOffset
		(
			ElementList,
			InLayer,
			InAllottedGeometry,
			InBrush,
			InTint,
			FVector2D(0, 0)
		);
	}

	static void DrawBrushCenterFitWithOffset(
		FSlateWindowElementList& ElementList,
		uint32 InLayer,
		const FGeometry& InAllottedGeometry,
		const FSlateBrush* InBrush,
		const FLinearColor& InTint,
		const FVector2D InOffset)
	{
		if (!InBrush)
		{
			return;
		}

		const FVector2D AreaSize = InAllottedGeometry.GetLocalSize();
		const FVector2D ProgressSize = InBrush->GetImageSize();
		const float FitScale = FMath::Min(FMath::Min(AreaSize.X / ProgressSize.X, AreaSize.Y / ProgressSize.Y), 1.0f);
		const FVector2D FinalSize = FitScale * ProgressSize;

		const FVector2D Offset = (InAllottedGeometry.GetLocalSize() * 0.5f) - (FinalSize * 0.5f) + InOffset;

		FSlateDrawElement::MakeBox
		(
			ElementList,
			InLayer,
			InAllottedGeometry.ToPaintGeometry(Offset, FinalSize),
			InBrush,
			ESlateDrawEffect::None,
			InTint
		);
	}
};



void FMeasuredText::SetText(const FText& InText)
{
	CachedText = InText;
	bTextDirty = true;
}

FVector2D FMeasuredText::UpdateTextSize(const FSlateFontInfo &InFontInfo, float FontScale) const
{
	if (bTextDirty)
	{
		bTextDirty = false;
		CachedTextSize = FSlateApplication::Get().GetRenderer()->GetFontMeasureService()->Measure(CachedText, InFontInfo, FontScale);
	}

	return CachedTextSize;
}

UCommonPlayerInputKey::UCommonPlayerInputKey(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
	, BoundKeyFallback(EKeys::Invalid)
	, InputTypeOverride(ECommonInputType::Count)
{
	FrameSize = FVector2D(0, 0);
}

void UCommonPlayerInputKey::NativePreConstruct()
{
	Super::NativePreConstruct();

	UpdateKeybindWidget();

	if (IsDesignTime())
	{
		ShowHoldBackPlate();
		RecalculateDesiredSize();
	}
}

void UCommonPlayerInputKey::NativeConstruct()
{
	Super::NativeConstruct();
}

void UCommonPlayerInputKey::NativeDestruct()
{
	if (ProgressPercentageMID)
	{
		// Need to restore the material on the brush before we kill off the MID.
		HoldProgressBrush.SetResourceObject(ProgressPercentageMID->GetMaterial());

		ProgressPercentageMID->MarkAsGarbage();
		ProgressPercentageMID = nullptr;
	}

	Super::NativeDestruct();
}

int32 UCommonPlayerInputKey::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
	int32 MaxLayer = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled);

	if (bDrawProgress)
	{
		FSlateDrawUtil::DrawBrushCenterFit
		(
			OutDrawElements,
			++MaxLayer,
			AllottedGeometry,
			&HoldProgressBrush,
			FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * HoldProgressBrush.GetTint(InWidgetStyle))
		);
	}

	if (bDrawCountdownText)
	{
		const FVector2D CountdownTextOffset = (AllottedGeometry.GetLocalSize() - CountdownText.GetTextSize()) * 0.5f;

		FSlateDrawElement::MakeText
		(
			OutDrawElements,
			++MaxLayer,
			AllottedGeometry.ToOffsetPaintGeometry(CountdownTextOffset),
			CountdownText.GetText(),
			CountdownTextFont,
			ESlateDrawEffect::None,
			FLinearColor(InWidgetStyle.GetColorAndOpacityTint())
		);
	}
	else if (bDrawBrushForKey)
	{
		// Draw Shadow
		FSlateDrawUtil::DrawBrushCenterFitWithOffset
		(
			OutDrawElements,
			++MaxLayer,
			AllottedGeometry,
			&CachedKeyBrush,
			FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * FLinearColor::Black),
			FVector2D(1, 1)
		);

		FSlateDrawUtil::DrawBrushCenterFit
		(
			OutDrawElements,
			++MaxLayer,
			AllottedGeometry,
			&CachedKeyBrush,
			FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * CachedKeyBrush.GetTint(InWidgetStyle))
		);
	}
	else if (KeybindText.GetTextSize().X > 0)
	{
		const FVector2D FrameOffset = (AllottedGeometry.GetLocalSize() - FrameSize) * 0.5f;

		FSlateDrawElement::MakeBox
		(
			OutDrawElements,
			++MaxLayer,
			AllottedGeometry.ToPaintGeometry(FrameOffset, FrameSize),
			&KeyBindTextBorder,
			ESlateDrawEffect::None,
			FLinearColor(InWidgetStyle.GetColorAndOpacityTint() * KeyBindTextBorder.GetTint(InWidgetStyle))
		);

		const FVector2D ActionTextOffset = (AllottedGeometry.GetLocalSize() - KeybindText.GetTextSize()) * 0.5f;

		FSlateDrawElement::MakeText
		(
			OutDrawElements,
			++MaxLayer,
			AllottedGeometry.ToOffsetPaintGeometry(ActionTextOffset),
			KeybindText.GetText(),
			KeyBindTextFont,
			ESlateDrawEffect::None,
			FLinearColor(InWidgetStyle.GetColorAndOpacityTint())
		);
	}

	return MaxLayer;
}

void UCommonPlayerInputKey::StartHoldProgress(FName HoldActionName, float HoldDuration)
{
	if (HoldActionName == BoundAction && ensureMsgf(HoldDuration > 0.0f, TEXT("Trying to perform hold action \"%s\" with no HoldDuration"), *BoundAction.ToString()))
	{
		HoldKeybindDuration = HoldDuration;
		HoldKeybindStartTime = GetWorld()->GetRealTimeSeconds();

		UpdateHoldProgress();
	}
}

void UCommonPlayerInputKey::StopHoldProgress(FName HoldActionName, bool bCompletedSuccessfully)
{
	if (HoldActionName == BoundAction)
	{
		HoldKeybindStartTime = 0.f;
		HoldKeybindDuration = 0.f;

		if (ensure(ProgressPercentageMID))
		{
			ProgressPercentageMID->SetScalarParameterValue(PercentageMaterialParameterName, 0.f);
		}

		if (bDrawCountdownText)
		{
			bDrawCountdownText = false;
			Invalidate(EInvalidateWidget::Paint);
			RecalculateDesiredSize();
		}
	}
}

void UCommonPlayerInputKey::SyncHoldProgress()
{
	// If we had an active hold action, stop it
	if (HoldKeybindStartTime > 0.f)
	{
		StopHoldProgress(BoundAction, false);
	}
}

void UCommonPlayerInputKey::UpdateHoldProgress()
{
	if (HoldKeybindStartTime != 0.f && HoldKeybindDuration > 0.f)
	{
		const float CurrentTime = GetWorld()->GetRealTimeSeconds();
		const float ElapsedTime = FMath::Min(CurrentTime - HoldKeybindStartTime, HoldKeybindDuration);
		const float RemainingTime = FMath::Max(0.0f, HoldKeybindDuration - ElapsedTime);

		if (ElapsedTime < HoldKeybindDuration && ensure(ProgressPercentageMID))
		{
			const float HoldKeybindPercentage = ElapsedTime / HoldKeybindDuration;
			ProgressPercentageMID->SetScalarParameterValue(PercentageMaterialParameterName, HoldKeybindPercentage);

			// Schedule a callback for next tick to update the hold progress again.
			GetWorld()->GetTimerManager().SetTimerForNextTick(this, &ThisClass::UpdateHoldProgress);		
		}

		if (bShowTimeCountDown)
		{
			FNumberFormattingOptions Options;
			Options.MinimumFractionalDigits = 1;
			Options.MaximumFractionalDigits = 1;
			CountdownText.SetText(FText::AsNumber(RemainingTime, &Options));

			bDrawCountdownText = true;
			Invalidate(EInvalidateWidget::Paint);
			RecalculateDesiredSize();
		}
	}
}

void UCommonPlayerInputKey::UpdateKeybindWidget()
{
	if (!GetOwningPlayer<ACommonPlayerController>())
	{
		bWaitingForPlayerController = true;
		return;
	}

	UCommonInputSubsystem* CommonInputSubsystem = GetInputSubsystem();

	if (CommonInputSubsystem && !CommonInputSubsystem->ShouldShowInputKeys())
	{
		SetVisibility(ESlateVisibility::Collapsed);
		return;
	}

	const bool bIsUsingGamepad = (InputTypeOverride == ECommonInputType::Gamepad) || ((CommonInputSubsystem != nullptr) && (CommonInputSubsystem->GetCurrentInputType() == ECommonInputType::Gamepad)) ;

	if (!BoundKey.IsValid())
	{
		BoundKey = BoundKeyFallback;
	}
	UE_LOG(LogCommonPlayerInput, Verbose, TEXT("UCommonKeybindWidget::UpdateKeybindWidget: Action: %s Key: %s"), *(BoundAction.ToString()), *(BoundKey.ToString()));

	// Must be called before Update, due to the creation of ProgressPercentageMID which will be used in Update
	SetupHoldKeybind();

	bool NewDrawBrushForKey = false;
	bool NeedToRecalcSize = false;

	if (BoundKey.IsValid())
	{
		SetVisibility(ESlateVisibility::HitTestInvisible);

		ShowHoldBackPlate();

		NeedToRecalcSize = true;
	}
	else
	{
		if (bShowUnboundStatus)
		{
			SetVisibility(ESlateVisibility::HitTestInvisible);
			NewDrawBrushForKey = false;

			KeybindText.SetText(LOCTEXT("Unbound", "Unbound"));

			NeedToRecalcSize = true;
		}
		else
		{
			SetVisibility(ESlateVisibility::Collapsed);
		}
	}

	if (bDrawBrushForKey != NewDrawBrushForKey)
	{
		bDrawBrushForKey = NewDrawBrushForKey;
		Invalidate(EInvalidateWidget::Paint);
	}

	// As RecalculateDesiredSize relies on the bDrawBrushForKey 
	// we shouldn't call it until that value has been finalized
	// for the update
	if (NeedToRecalcSize)
	{
		RecalculateDesiredSize();
	}
}

void UCommonPlayerInputKey::SetBoundKey(FKey NewKey)
{
	if (NewKey != BoundKey)
	{
		BoundKeyFallback = NewKey;
		BoundAction = NAME_None;
		UpdateKeybindWidget();
	}
}

void UCommonPlayerInputKey::SetBoundAction(FName NewBoundAction)
{
	bool bUpdateWidget = true;

	if (BoundAction != NewBoundAction)
	{
		BoundAction = NewBoundAction;
	}

	if (bUpdateWidget)
	{
		UpdateKeybindWidget();
	}
}

void UCommonPlayerInputKey::NativeOnInitialized()
{
	Super::NativeOnInitialized();

	if (UCommonLocalPlayer* CommonLocalPlayer = GetOwningLocalPlayer<UCommonLocalPlayer>())
	{
		CommonLocalPlayer->OnPlayerControllerSet.AddUObject(this, &ThisClass::HandlePlayerControllerSet);
	}
}

void UCommonPlayerInputKey::SetForcedHoldKeybind(bool InForcedHoldKeybind)
{
	if (InForcedHoldKeybind)
	{
		SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus::ForcedHold);
	}
	else
	{
		SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus::NoForcedHold);
	}
}

void UCommonPlayerInputKey::SetForcedHoldKeybindStatus(ECommonKeybindForcedHoldStatus InForcedHoldKeybindStatus)
{
	ForcedHoldKeybindStatus = InForcedHoldKeybindStatus;

	UpdateKeybindWidget();
}

void UCommonPlayerInputKey::SetShowProgressCountDown(bool bShow)
{
	bShowTimeCountDown = bShow;
}

void UCommonPlayerInputKey::SetupHoldKeybind()
{
	ACommonPlayerController* OwningCommonPlayer = Cast<ACommonPlayerController>(GetOwningPlayer());

	// Setup the hold
	if (ForcedHoldKeybindStatus == ECommonKeybindForcedHoldStatus::ForcedHold)
	{
		bIsHoldKeybind = true;
	}
	else if (ForcedHoldKeybindStatus == ECommonKeybindForcedHoldStatus::NeverShowHold)
	{
		bIsHoldKeybind = false;
	}

	if (ensure(OwningCommonPlayer))
	{
		if (bIsHoldKeybind)
		{
			// Setup the ProgressPercentageMID
			if (ProgressPercentageMID == nullptr)
			{
				if (UMaterialInterface* Material = Cast<UMaterialInterface>(HoldProgressBrush.GetResourceObject()))
				{
					ProgressPercentageMID = UMaterialInstanceDynamic::Create(Material, this);
					HoldProgressBrush.SetResourceObject(ProgressPercentageMID);
				}
			}
			SyncHoldProgress();
		}
	}
}

void UCommonPlayerInputKey::ShowHoldBackPlate()
{
	bool bDirty = false;

	if (IsHoldKeybind())
	{
		float BrushSizeAsValue = 32.0f;
		
		float DesiredBoxSize = BrushSizeAsValue + 10.0f;
		if (!bDrawBrushForKey)
		{
			DesiredBoxSize += 14.0f;
		}

		const FVector2D NewDesiredBrushSize(DesiredBoxSize, DesiredBoxSize);
		if (HoldProgressBrush.GetImageSize() != NewDesiredBrushSize)
		{
			HoldProgressBrush.SetImageSize(NewDesiredBrushSize);
			bDirty = true;
		}

		if (!bDrawProgress)
		{
			bDrawProgress = true;
			bDirty = true;
		}

		static const FName BackAlphaName = TEXT("BackAlpha");
		static const FName OutlineAlphaName = TEXT("OutlineAlpha");

		if (ProgressPercentageMID)
		{
			ProgressPercentageMID->SetScalarParameterValue(BackAlphaName, 0.2f);
			ProgressPercentageMID->SetScalarParameterValue(OutlineAlphaName, 0.4f);
		}
	}
	else
	{
		if (bDrawProgress)
		{
			bDrawProgress = false;
			bDirty = true;
		}
	}

	if (bDirty)
	{
		Invalidate(EInvalidateWidget::Paint);
	}
}

void UCommonPlayerInputKey::HandlePlayerControllerSet(UCommonLocalPlayer* LocalPlayer, APlayerController* PlayerController)
{
	if (bWaitingForPlayerController && GetOwningPlayer<ACommonPlayerController>())
	{
		UpdateKeybindWidget();
		bWaitingForPlayerController = false;
	}
}

void UCommonPlayerInputKey::RecalculateDesiredSize()
{
	FVector2D MaximumDesiredSize(0, 0);
	float LayoutScale = 1;

	if (bDrawProgress)
	{
		MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, HoldProgressBrush.GetImageSize());
	}

	if (bDrawCountdownText)
	{
		MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, CountdownText.UpdateTextSize(CountdownTextFont, LayoutScale));
	}
	else if (bDrawBrushForKey)
	{
		MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, CachedKeyBrush.GetImageSize());
	}
	else
	{
		const FVector2D KeybindTextSize = KeybindText.UpdateTextSize(KeyBindTextFont, LayoutScale);
		FrameSize = FVector2D::Max(KeybindTextSize, KeybindFrameMinimumSize) + KeybindTextPadding.GetDesiredSize();
		MaximumDesiredSize = FVector2D::Max(MaximumDesiredSize, FrameSize);
	}

	SetMinimumDesiredSize(MaximumDesiredSize);
}

#undef LOCTEXT_NAMESPACE