// Project Sword & Gun Copyright © 2021 Kevin Poretti


#include "Controllers/SNGPlayerController.h"

#include "DrawDebugHelpers.h"
#include "Kismet/KismetMathLibrary.h"

void ASNGPlayerController::OnPossess(APawn* aPawn)
{
	Super::OnPossess(aPawn);
	
	if(Protagonist == nullptr)
	{
		Protagonist = Cast<ASNGProtagonist>(GetCharacter());
	}
	
	SetupInputs();
}

void ASNGPlayerController::OnUnPossess()
{
	Protagonist = nullptr;
}

void ASNGPlayerController::SetupInputs()
{
	check(Protagonist != nullptr);

	InputComponent->BindAxis("Turn", this, &ASNGPlayerController::AddYawInput);
	InputComponent->BindAxis("LookUp", this, &ASNGPlayerController::AddPitchInput);

	// have to figure out how to decouple these
	InputComponent->BindAxis("MoveForward", Protagonist, &ASNGProtagonist::MoveForward);
	InputComponent->BindAxis("MoveRight", Protagonist, &ASNGProtagonist::MoveRight);

	RegisterAction("Jump", "JUMP");
	RegisterAction("Dash", "DASH");
	RegisterAction("Attack", "ATTACK");
	RegisterAction("Fire", "FIRE");
	RegisterAction("Reload", "RELOAD");
	RegisterAction("UseGadget", "USE_GADGET");
	//BindActionToCharacterCommand("CycleMeleeWeapon", "CYCLE_MELEE");
	//BindActionToCharacterCommand("CycleRangedWeapon", "CYCLE_RANGED");

	InputComponent->BindAction("CycleRangedWeapon", IE_Pressed, Protagonist, &ASNGProtagonist::CycleRangedWeapon);
	InputComponent->BindAction("CycleMeleeWeapon", IE_Pressed, Protagonist, &ASNGProtagonist::CycleMeleeWeapon);
	//InputComponent->BindAction("Attack", IE_Pressed, this, &ASNGPlayerController::ProcessAttackInput);
}

void ASNGPlayerController::RegisterAction(FName ActionName, FName CommandName)
{
	ActionToCharacterCommand.Add(ActionName, CommandName);
	BindAddInputAction(ActionName, EActionState::PRESSED, EInputEvent::IE_Pressed);
	// NOTE(kevin):
	// Repeat doesn't seem to work as I was expecting. There seems to be a delay before the button is considered repeating.
	// Seems to work more like how text editor input works where if you hold the key after a while you will get repeating
	// characters for that key.
	// At some point I think we'll need to just need to poll inputs ourselves in the Tick function.
	// Only problem is polling only seems to work for Keys and not Actions. We will need to do something to get the
	// key(s) that are associated with an action and poll for those and then translate those into character commands.
	// All the while we need to make sure that if the user rebinds the key associated with an action
	// (through an in game menu or something) at runtime that we are updating that mapping here so we poll for the correct
	// key.
	BindAddInputAction(ActionName, EActionState::HELD, EInputEvent::IE_Repeat);
	BindAddInputAction(ActionName, EActionState::RELEASED, EInputEvent::IE_Released);
}

void ASNGPlayerController::BindAddInputAction(FName ActionName, EActionState ActionState, EInputEvent InputEvent)
{
	check(InputComponent != nullptr);
	
	FInputActionHandlerSignature ActionHandler;
	ActionHandler.BindUObject(this, &ASNGPlayerController::AddInputActionToBuffer, ActionName, ActionState);
	
	FInputActionBinding ActionBinding(ActionName, InputEvent);
	ActionBinding.ActionDelegate = ActionHandler;
	InputComponent->AddActionBinding(ActionBinding);
}

/*
void ASNGPlayerController::BindActionToCharacterCommand(FName ActionName, FName CommandName)
{
	check(InputComponent != nullptr);
	
	FInputActionHandlerSignature ActionHandler;
	ActionHandler.BindUObject(this, &ASNGPlayerController::PushCharacterCommandToPawn, CommandName);
	
	FInputActionBinding ActionBinding(ActionName, IE_Pressed);
	ActionBinding.ActionDelegate = ActionHandler;
	InputComponent->AddActionBinding(ActionBinding);
}
*/

void ASNGPlayerController::AddInputActionToBuffer(FName ActionName, EActionState State)
{
	FInputAction MyInputAction;
	MyInputAction.Name = ActionName;
	MyInputAction.State = State;
	CurrInput.Actions.Add(MyInputAction);
}


void ASNGPlayerController::Tick(float DeltaSeconds)
{
	for (auto SpecialCommand : SpecialCommands)
	{
		FString DebugMsg = FString::Printf(TEXT("SpecialCommand %s CurrIdx %d"), *SpecialCommand.Name.ToString(), SpecialCommand.CurrCommandIdx);
		GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Magenta, DebugMsg);

		FString TimeInStateMsg = FString::Printf(TEXT("SpecialCommand %s TimeInState %s"), *SpecialCommand.Name.ToString(), *FString::SanitizeFloat(SpecialCommand.CurrTime));
		GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Cyan, TimeInStateMsg);
	}
	
	for(int idx = 0; idx < Buffer.Num(); idx++)
	{
		// remove stale buffered input frames
		FBufferedInput MyBuffInput = Buffer[idx];
		if(MyBuffInput.TimeAdded < GetWorld()->TimeSeconds - MaxInputLifetime)
		{
			Buffer.RemoveAt(idx);
			continue;
		}

		ProcessInputFrame(Buffer[idx]);
		//ProcessActions(Buffer[idx].Actions);
	}

	CurrInput.Stick.X = InputComponent->GetAxisValue("MoveRight");
	CurrInput.Stick.Y = InputComponent->GetAxisValue("MoveForward");

	// convert stick input into cardinal directions
	FVector WorldDir = GetInputAsWorldDirection(CurrInput.Stick.Y, CurrInput.Stick.X);
	APawn* MyPawn = GetPawn();
	check(MyPawn);
	WorldDir.Normalize();
	/*
	DrawDebugDirectionalArrow(GetWorld(), MyPawn->GetActorLocation(), MyPawn->GetActorLocation() + (WorldDir * 100.0f),
	10.0f, FColor::Magenta, false, -1.0f, 0, 2.0f);
	FVector PawnDir = UKismetMathLibrary::GetForwardVector(PawnRot);
	FRotator ControllerPawnAngle = UKismetMathLibrary::MakeRotFromX()'
	*/
	FRotator InputRot = UKismetMathLibrary::MakeRotFromX(WorldDir);
	FRotator DeltaRot = UKismetMathLibrary::NormalizedDeltaRotator(InputRot, MyPawn->GetActorRotation());
	/*
	FString DebugMsg = FString::Printf(TEXT("Controller Rot: %s"), *FString::SanitizeFloat(DeltaRot.Yaw));
	GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, DebugMsg);
	*/
	// this definitely isn't a good way to do this
	ERawInputs CardinalDir = ERawInputs::NEUTRAL;
	if(!WorldDir.IsNearlyZero())
	{
		if(DeltaRot.Yaw >= -45 && DeltaRot.Yaw <= 45)
		{
			CardinalDir = ERawInputs::FORWARD;
		}
		else if (DeltaRot.Yaw >= 45 && DeltaRot.Yaw <= 135)
		{
			CardinalDir = ERawInputs::RIGHT;
		}
		else if (DeltaRot.Yaw <= -45 && DeltaRot.Yaw >= -135)
		{
			CardinalDir = ERawInputs::LEFT;
		}
		else
		{
			CardinalDir = ERawInputs::BACK;
		}
	}

	CurrInput.Direction = CardinalDir;
	CurrInput.TimeAdded = GetWorld()->TimeSeconds;
	// process current action this frame if possible
	ProcessInputFrame(CurrInput);
	//ProcessActions(CurrInput.Actions);
	
	Buffer.Add(CurrInput);
	CurrInput = FBufferedInput();
}

void ASNGPlayerController::PushCharacterCommandToPawn(FName Command)
{
	FSNGEvent Event;
	Event.EventType = ESNGEventType::CHARACTER_COMMAND;
	Event.CommandData.CommandName	= Command;
	
	// push event
	if(Protagonist->Implements<USNGEventProcessorInterface>())
	{
		ISNGEventProcessorInterface* EventProcessor = Cast<ISNGEventProcessorInterface>(Protagonist);
		EventProcessor->PushEvent(Event);
	}
}

void ASNGPlayerController::ProcessInputFrame(FBufferedInput& InputFrame)
{
	// check if this input frame's direction advances the state of a special command
	// this also needs to check if the command state needs to get reset because of time reset
	for (int idx = 0 ; idx < SpecialCommands.Num(); idx++)
	{
		FSpecialCommand& SpecialCommand = SpecialCommands[idx];
		check(SpecialCommand.CurrCommandIdx <= SpecialCommand.Commands.Num())
		ERawInputs CurrCommand = SpecialCommand.Commands[SpecialCommand.CurrCommandIdx];

		SpecialCommand.CurrTime += GetWorld()->GetDeltaSeconds();
		if(SpecialCommand.CurrTime >= SpecialCommand.TimeToReset)
		{
			SpecialCommand.CurrCommandIdx = 0;
			SpecialCommand.CurrTime = 0.0f;
		}
		
		if(InputFrame.Direction == CurrCommand)
		{
			UE_LOG(LogTemp, Warning, TEXT("Direction incremented state of special command %s"), *SpecialCommand.Name.ToString());
			SpecialCommand.CurrCommandIdx++;
			SpecialCommand.CurrTime = 0.0f;
		}

		// we got a valid special command so trigger that and stop processing
		// this case will only trigger if your command ends with a direction input (like forward, right, back, left)
		// do we actually need to stop processing?
		if(SpecialCommand.CurrCommandIdx == SpecialCommand.Commands.Num())
		{
			UE_LOG(LogTemp, Warning, TEXT("Direction triggered special command %s"), *SpecialCommand.Name.ToString());
			PushCharacterCommandToPawn(SpecialCommand.Name);
			SpecialCommand.CurrCommandIdx = 0;
			return;
		}
	}
	
	// check if this input frame's actions advances the state of a special command
	for (auto InputAction : InputFrame.Actions)
	{
		// try to process input
		if(!InputAction.IsConsumed)
		{
			//check(ActionToCharacterCommand.Contains(InputAction.Name));
			FName CharacterCommand = ActionToCharacterCommand[InputAction.Name]; 
			switch (InputAction.State)
			{
			case EActionState::PRESSED:
				// check if this input frame's direction advances the state of a special command
				for (int idx = 0 ; idx < SpecialCommands.Num(); idx++)
				{
					FSpecialCommand& SpecialCommand = SpecialCommands[idx];
					check(SpecialCommand.CurrCommandIdx < SpecialCommand.Commands.Num())
					ERawInputs CurrCommand = SpecialCommand.Commands[SpecialCommand.CurrCommandIdx];
					// HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
					// HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
					// HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
					if((InputAction.Name == "Attack" && CurrCommand == ERawInputs::ATTACK) ||
						(InputAction.Name == "Dash" && CurrCommand == ERawInputs::DASH))
					{
						UE_LOG(LogTemp, Warning, TEXT("Action incremented state of special command %s"), *SpecialCommand.Name.ToString());
						SpecialCommand.CurrCommandIdx++;
						SpecialCommand.CurrTime = 0.0f;
					}
					
					if(SpecialCommand.CurrCommandIdx == SpecialCommand.Commands.Num())
					{
						UE_LOG(LogTemp, Warning, TEXT("Action triggered special command %s"), *SpecialCommand.Name.ToString());
						PushCharacterCommandToPawn(SpecialCommand.Name);
						SpecialCommand.CurrCommandIdx = 0;
						return;
					}
				}
				//CharacterCommand = The special command
				PushCharacterCommandToPawn(CharacterCommand);
				// NOTE(kevin) remove this later
				{
					FString DebugMsg = FString::Printf(TEXT("Action %s is being pressed"), *InputAction.Name.ToString());
					GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Red, DebugMsg);
				}
				break;
			case EActionState::HELD:
				// NOTE(kevin) remove this later
				{
					FString DebugMsg = FString::Printf(TEXT("Action %s is being held"), *InputAction.Name.ToString());
					GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, DebugMsg);
				}
				break;
			case EActionState::RELEASED:
				// NOTE(kevin) remove this later
				{
					FString DebugMsg = FString::Printf(TEXT("Action %s is being released"), *InputAction.Name.ToString());
					GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Blue, DebugMsg);
				}
				break;
			}
		}
	}
}

/*
void ASNGPlayerController::ProcessActions(TArray<FInputAction>& Actions)
{
	for (auto InputAction : Actions)
	{
		// try to process input
		if(!InputAction.IsConsumed)
		{
			check(ActionToCharacterCommand.Contains(InputAction.Name));
			FName CharacterCommand = ActionToCharacterCommand[InputAction.Name]; 
			switch (InputAction.State)
			{
			case EActionState::PRESSED:
				PushCharacterCommandToPawn(CharacterCommand);
				// NOTE(kevin) remove this later
				{
					FString DebugMsg = FString::Printf(TEXT("Action %s is being pressed"), *InputAction.Name.ToString());
					GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Red, DebugMsg);
				}
				break;
			case EActionState::HELD:
				// NOTE(kevin) remove this later
				{
					FString DebugMsg = FString::Printf(TEXT("Action %s is being held"), *InputAction.Name.ToString());
					GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, DebugMsg);
				}
				break;
			case EActionState::RELEASED:
				// NOTE(kevin) remove this later
				{
					FString DebugMsg = FString::Printf(TEXT("Action %s is being released"), *InputAction.Name.ToString());
					GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Blue, DebugMsg);
				}
				break;
			}
		}
	}
}
*/

/*
ERawInputs ASNGPlayerController::WorldInputDirectionToCardinalDirection(FVector WorldInputDirection)
{
	NEUTRAL,
	BACK,
	FORWARD,
	LEFT,
	RIGHT,
}
*/

FVector ASNGPlayerController::GetInputAsWorldDirection(float ForwardAxisValue, float RightAxisValue)
{
	if(FMath::Abs(ForwardAxisValue) == 0.0f && FMath::Abs(RightAxisValue) == 0.0f)
	{
		return FVector::ZeroVector;
	}

	FRotator ViewRotation(0.0f, GetControlRotation().Yaw, 0.0f);
	FVector ForwardInput = UKismetMathLibrary::GetForwardVector(ViewRotation) * ForwardAxisValue;
	FVector RightInput = UKismetMathLibrary::GetRightVector(ViewRotation) * RightAxisValue;
	return ForwardInput + RightInput;
}
