// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.

#include "Components/InputBufferComponent.h"

#include "ComboInputAssets.h"
#include "EnhancedInputComponent.h"
#include "Kismet/GameplayStatics.h"

#include "Components/ComboManagerComponent.h"
#include "GlobalSettings/InputBufferGlobalSettings.h"

DEFINE_LOG_CATEGORY(LogInputBufferComponent);


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

	if (APlayerController *PlayerController = UGameplayStatics::GetPlayerController(this, 0))
	{
		if (UComboManagerComponent *ComboManager = PlayerController->GetComponentByClass<UComboManagerComponent>())
		{
			// Get the player character and try to connect to its combo manager.
			this->OnNewComboInput.BindUObject(ComboManager, &UComboManagerComponent::HandleComboInput);

			// Get all unique EnhancedInput actions bound to combo input actions.
			const UInputBufferGlobalSettings *Settings = GetDefault<UInputBufferGlobalSettings>();
			TSet<const UInputAction *> InputActionsToBind;
			for (TSoftObjectPtr<const UComboInputAsset> ComboInput : Settings->ComboInputs)
			{
				if (ComboInput.IsValid())
				{
					this->ComboInputList.Emplace(ComboInput.Get());
					for (const UInputAction *InputAction : ComboInput->ActionGroup)
					{
						InputActionsToBind.Add(InputAction);
					}
				}
				else
				{
					UE_LOG(LogInputBufferComponent, Verbose, TEXT("Invalid combo action found in Combo Actions list in %s"), *Settings->GetClass()->GetName());
				}
			}
			for (const UInputAction *InputAction : InputActionsToBind)
			{
				this->BindAction(InputAction, ETriggerEvent::Started, this, &UInputBufferComponent::AddActionToBuffer, InputAction);
				this->BindAction(InputAction, ETriggerEvent::Completed, this, &UInputBufferComponent::ExpireAction, InputAction);
			}
		}
	}
}


void UInputBufferComponent::AddActionToBuffer(const FInputActionValue &Value, const class UInputAction *Action)
{
	this->MostRecentActions.Add(Action);
	this->ExpiringActions.Remove(Action);

	// Find any combo input that matches this action, plus buffered actions.
	for (TObjectPtr<const UComboInputAsset> ComboInput : this->ComboInputList)
	{
		if (ComboInput->MatchesInputActions(this->MostRecentActions))
		{
			this->ActivateComboInput(ComboInput.Get());

			break;
		}
	}

	const UInputBufferGlobalSettings *Settings = GetDefault<UInputBufferGlobalSettings>();
	this->GetWorld()->GetTimerManager().SetTimer(this->MultiPressTimerHandle, this, &UInputBufferComponent::ClearMultiPresses, Settings->MultiPressTimerLength);
}
void UInputBufferComponent::ClearMultiPresses()
{
#if WITH_EDITOR
	TArray<FString> ActionNames;
	for (const UInputAction *Action : this->MostRecentActions)
	{
		ActionNames.Add(Action->GetName());
	}
	UE_LOG(LogInputBufferComponent, Verbose, TEXT("Multi-press buffer cleared (%s)"), *FString::Join(ActionNames, TEXT(" | ")));
#endif
	this->MostRecentActions.Empty();
}

void UInputBufferComponent::ActivateComboInput(const UComboInputAsset *ComboInput)
{
	checkf(ComboInput, TEXT("Invalid %s."), *UComboInputAsset::StaticClass()->GetName());

	const bool bNewInputLocked = this->LockedComboInputs.Contains(ComboInput);
	const bool bNewSupercedesActive = (this->InputBufferActive && this->InputBufferActive->ContainsOneOf(ComboInput->ActionGroup) && !bNewInputLocked);

	// Make this combo input active if it isn't being locked, or if we are
	// overwriting a previous combo input with a multi-press combo input.
	if (bNewSupercedesActive || !bNewInputLocked)
	{
		// Set the combo input as active.
		this->InputBufferActive = ComboInput;
		this->InputBufferHold = nullptr;

		const UInputBufferGlobalSettings *Settings = GetDefault<UInputBufferGlobalSettings>();
		for (const TObjectPtr<const UComboInputAsset> &LockedAsset : this->ComboInputList)
		{
			this->LockComboInput(LockedAsset);
		}

		this->OnNewComboInput.Execute(ComboInput, EComboActionTriggerEvent::Activated);

		// If the incoming combo input contains an expiring action,
		// we know it's been released, so send that signal too.
		if (ComboInput->ContainsOneOf(this->ExpiringActions))
		{
			this->OnNewComboInput.Execute(ComboInput, EComboActionTriggerEvent::Released);
		}

		UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s is active."), *ComboInput->ComboInputName.ToString());
	}
	else
	{
		this->HoldComboInput(ComboInput);
	}
}

void UInputBufferComponent::HoldComboInput(const UComboInputAsset *ComboInput)
{
	this->InputBufferHold = ComboInput;
	UE_SUPPRESS(LogInputBufferComponent, Verbose,
		{
			if (this->LockedComboInputs.Contains(ComboInput))
			{
				UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s is locked and won't be activated yet."), *ComboInput->ComboInputName.ToString());
			}
			else
			{
				UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s added to buffer."), *ComboInput->ComboInputName.ToString());
			}
		}
	);
}

void UInputBufferComponent::LockComboInput(const UComboInputAsset *Input)
{
	this->LockedComboInputs.Emplace(Input);
}

void UInputBufferComponent::UnlockComboInput(const UComboInputAsset *Unlocked)
{
	// Remove the newly-unlocked asset from the locked combo inputs.
	UE_CLOG(this->LockedComboInputs.Contains(Unlocked), LogInputBufferComponent, Verbose, TEXT("%s has unlocked."), *Unlocked->ComboInputName.ToString());
	this->LockedComboInputs.Remove(Unlocked);

	// Check if the newly unlocked combo input is in the hold.
	if (Unlocked == this->InputBufferHold)
	{
		const UComboInputAsset *OriginalActive = this->InputBufferActive;

		// Activate the held combo input.
		const UComboInputAsset *HeldAsset = this->InputBufferHold;
		this->InputBufferHold = nullptr;
		if (HeldAsset)
		{
			this->ActivateComboInput(HeldAsset);
		}

		UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s has expired."), *OriginalActive->ComboInputName.ToString());
	}
}

void UInputBufferComponent::ExpireAction(const FInputActionValue &Value, const class UInputAction *Action)
{
	// Only send a release event if we haven't already, i.e. if the combo input associated
	// with the action is not already buffered for another future activation.
	if (this->InputBufferActive && this->InputBufferActive != this->InputBufferHold && this->InputBufferActive->MatchesInputAction(Action))
	{
		this->OnNewComboInput.Execute(this->InputBufferActive, EComboActionTriggerEvent::Released);
	}

	// Prepare to expire any buffered combo inputs
	this->ExpiringActions.Add(Action);
	const UInputBufferGlobalSettings *Settings = GetDefault<UInputBufferGlobalSettings>();
	this->GetWorld()->GetTimerManager().SetTimer(this->InputReleaseExpirationTimerHandle, this, &UInputBufferComponent::ExpireBufferedActions, Settings->InputReleaseExpirationTimerLength);
}
void UInputBufferComponent::ExpireBufferedActions()
{
	// Only bother dealing with this if there's something to deal with in the first place.
	if (!this->ExpiringActions.IsEmpty())
	{
		UE_SUPPRESS(LogInputBufferComponent, Verbose,
			{
				TArray<FString> ActionNames;
				for (const UInputAction *Action : this->ExpiringActions)
				{
					ActionNames.Add(Action->GetName());
				}
				UE_LOG(LogInputBufferComponent, Verbose, TEXT("Released actions expired (%s)"), *FString::Join(ActionNames, TEXT(" | ")));
			}
		);

		// If there is an action in the hold, check if it's related
		// to our current released buttons, and if so cancel it.
		if (this->InputBufferHold)
		{
			for (const UInputAction *Action : this->ExpiringActions)
			{
				if (this->InputBufferHold->MatchesInputAction(Action))
				{
					UE_LOG(LogInputBufferComponent, Verbose, TEXT("%s has been cancelled."), *this->InputBufferHold->ComboInputName.ToString());
					this->InputBufferHold = nullptr;
					break;
				}
			}
		}

		this->ExpiringActions.Empty();
	}
}
