// Oleg Petruny proprietary.

#include "PlayerBase.h"

#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "InputMappingContext.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMathLibrary.h"

#include "CustomGameSettings.h"
#include "CustomPlayerController.h"
#include "Interactable/Activators/InteractableActivator.h"
#include "Interactable/Interactable.h"
#include "Interactable/Modificators/InteractableModificator.h"
#include "MainGameModeBase.h"
#include "Widgets/WidgetsManager.h"

namespace
{
    const FVector cameraLocation = { 10, 0, 70 };
}

APlayerBase::APlayerBase()
    : ACharacter()
{
    static ConstructorHelpers::FObjectFinder<UInputMappingContext> asset{ TEXT("/Script/EnhancedInput.InputMappingContext'/Game/Input/IMC_Player.IMC_Player'") };
    context = asset.Object;
    ACustomPlayerController::AppendInputContext(context);

    PrimaryActorTick.bCanEverTick = true;

    camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
    camera->SetRelativeLocation(cameraLocation);
    camera->SetRelativeScale3D(FVector{ 0.25, 0.25, 0.25 });
    camera->bUsePawnControlRotation = true;
    camera->SetupAttachment(RootComponent);

    itemDropSpringArm = CreateDefaultSubobject<USpringArmComponent>(TEXT("ItemDropSpringArm"));
    itemDropSpringArm->TargetArmLength = -200;
    itemDropSpringArm->SetupAttachment(camera);
}

void APlayerBase::Tick(float DeltaTime)
{
    ACharacter::Tick(DeltaTime);

    // stabilize move speed by fps
    AddActorLocalOffset(ConsumeMovementInputVector() * DeltaTime);

    // update isMoving flag
    auto loc = GetActorLocation();
    bIsMoving = (loc != oldLocation);
    oldLocation = std::move(loc);

    rotationInput = FRotator::ZeroRotator;
}

void APlayerBase::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
    ACharacter::SetupPlayerInputComponent(PlayerInputComponent);
}

APlayerBase* APlayerBase::Get()
{
    if(auto PC = ACustomPlayerController::Get())
        return Cast<APlayerBase>(PC->GetPawn());
    return nullptr;
}

void APlayerBase::BeginPlay()
{
    ACharacter::BeginPlay();

    auto world = GetWorld();

    oldLocation = GetActorLocation();
    GetCharacterMovement()->MaxWalkSpeed = moveSpeed;

    cameraManager = UGameplayStatics::GetPlayerCameraManager(world, 0);
    if(cameraManager)
    {
        cameraManager->ViewPitchMin = minPitch;
        cameraManager->ViewPitchMax = maxPitch;
    }

    auto gameSettings = UCustomGameSettings::Get();
    if(gameSettings && camera)
    {
        camera->PostProcessSettings.MotionBlurAmount = gameSettings->bUseMotionBlur ? 1.0f : 0.0f;
    }

    LoadInteractablesActivators();
}

bool APlayerBase::IsMoving()
{
    return bIsMoving;
}

bool APlayerBase::IsRunning()
{
    return GetCharacterMovement()->MaxWalkSpeed > moveSpeed;
}

void APlayerBase::LockPlayer(FPlayerLock lock)
{
    walkLocked += lock.walk;
    jumpLocked += lock.jump;
    runLocked += lock.run;
    interactionLocked += lock.interaction;
    cameraLocked += lock.camera;
    inventoryLocked += lock.inventory;

    if(interactionLocked)
        ResetInteractions();
}

void APlayerBase::UnlockPlayer(FPlayerLock lock)
{
    walkLocked -= lock.walk;
    jumpLocked -= lock.jump;
    runLocked -= lock.run;
    interactionLocked -= lock.interaction;
    cameraLocked -= lock.camera;
    inventoryLocked -= lock.inventory;

    if(!interactionLocked)
        ResetInteractions();
}

void APlayerBase::FlyMode(bool on)
{
    GetCharacterMovement()->GravityScale = on ? 0.0f : 1.0f;
}

void APlayerBase::ResetInteractions()
{
    InteractableDeactivated(lastInteractable, static_cast<EActivatorType>(0xFF));
    for(auto activator : interactableActivators)
        activator->Rescan();
}

FVector APlayerBase::GetCameraDirection()
{
    return camera->GetForwardVector();
}
FVector APlayerBase::GetCameraLocation()
{
    return camera->GetComponentLocation();
}

void APlayerBase::SwitchToCameraPawn()
{
    if(auto gamemode_base = UGameplayStatics::GetGameMode(GetWorld()))
    {
        if(auto gamemode = Cast<AMainGameModeBase>(gamemode_base))
        {
            gamemode->SwitchCameraMode();
        }
    }
}

void APlayerBase::SwitchToView(AActor* target)
{
    camera->Deactivate();
    ((APlayerController*)Controller)->SetViewTargetWithBlend(target, 0.3f);
}

void APlayerBase::ReturnPlayerView()
{
    camera->Activate(false);
    ((APlayerController*)Controller)->SetViewTargetWithBlend(this, 0.3f);
}

void APlayerBase::MoveCamera(FVector2D value)
{
    if(cameraLocked)
        return;

    AddControllerYawInput(value.X);
    rotationInput.Yaw = value.X;
    AddControllerPitchInput(value.Y);
    rotationInput.Pitch = value.Y;
}

void APlayerBase::MoveCharacter(FVector2D value)
{
    if(walkLocked)
        return;

    moveVector = GetActorRightVector() * value.X;
    moveVector += GetActorForwardVector() * value.Y;
    moveVector.Normalize(UE_KINDA_SMALL_NUMBER);
    AddMovementInput(moveVector);
}

void APlayerBase::Jump()
{
    if(jumpLocked)
        return;

    ACharacter::Jump();
}

void APlayerBase::SwitchRun(bool run)
{
    if(runLocked)
        return;

    if(run)
        GetCharacterMovement()->MaxWalkSpeed = moveSpeed * runSpeedMultiplier;
    else
        GetCharacterMovement()->MaxWalkSpeed = moveSpeed;
}

void APlayerBase::UpdatePitch(float min, float max)
{
    minPitch = min;
    maxPitch = max;

    if(cameraManager)
    {
        cameraManager->ViewPitchMin = minPitch;
        cameraManager->ViewPitchMax = maxPitch;
    }
}

void APlayerBase::LoadInteractablesActivators()
{
    TSet<UClass*> instancedActivators;
    for(auto& act : AInteractable::interactionActivators)
    {
        if(act.Get())
            if(auto obj = Cast<UInteractableActivator>(act.Get()->GetDefaultObject()))
                if(!obj->AutoInstantiateInPlayer())
                    continue;

        if(instancedActivators.Contains(act.Get()))
            continue;
        instancedActivators.Add(act.Get());

        auto component = NewObject<UInteractableActivator>(this, act);
        component->interactableActivatedDelegate.BindUObject(this, &APlayerBase::InteractableActivated);
        component->interactableDeactivatedDelegate.BindUObject(this, &APlayerBase::InteractableDeactivated);
        component->SetupAttachment(camera);
        component->RegisterComponent();
        interactableActivators.Add(component);
    }
}

void APlayerBase::InteractableActivated(AInteractable* interactable, EActivatorType type)
{
    if(!interactable)
        return;

    if(interactionLocked || interactable->IsHidden())
        return;

    interactable->Activate(type);

    if(interactable != lastInteractable)
        lastInteractable = interactable;
}

void APlayerBase::InteractableDeactivated(AInteractable* interactable, EActivatorType type)
{
    if(!interactable)
        return;

    interactable->Deactivate(type);

    if(interactable->GetActivatedFlags() == 0)
        lastInteractable = nullptr;
}

bool APlayerBase::IsInInventory(const AActor* actor)
{
    return (actor == leftPocketItem) || (actor == rightPocketItem);
}

void APlayerBase::TakeToInventory(AActor* actor)
{
    actor->SetActorHiddenInGame(true);
    actor->SetActorEnableCollision(false);
    actor->SetActorTickEnabled(false);
    actor->AttachToComponent(itemDropSpringArm, FAttachmentTransformRules::SnapToTargetNotIncludingScale);
}

void APlayerBase::TakeItemToLeftHand(AActor* actor)
{
    if(inventoryLocked || itemDropLocked)
        return;

    TakeToInventory(actor);
    itemDropSpringArm->TickComponent(0, ELevelTick::LEVELTICK_All, nullptr);
    DropLeftHandItem();
    leftPocketItem = actor;
    itemDropLocked = true;
    GetWorld()->GetTimerManager().SetTimer(itemDropLockTimer, [&]() { itemDropLocked = false; }, 0.25f, false);
}

void APlayerBase::TakeItemToRightHand(AActor* actor)
{
    if(inventoryLocked || itemDropLocked)
        return;

    TakeToInventory(actor);
    itemDropSpringArm->TickComponent(0, ELevelTick::LEVELTICK_All, nullptr);
    DropRightHandItem();
    rightPocketItem = actor;
    itemDropLocked = true;
    GetWorld()->GetTimerManager().SetTimer(itemDropLockTimer, [&]() { itemDropLocked = false; }, 0.25f, false);
}

void APlayerBase::DropItem(AActor* actor)
{
    actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
    actor->SetActorHiddenInGame(false);
    actor->SetActorEnableCollision(true);
    actor->SetActorTickEnabled(true);
}

void APlayerBase::DropLeftHandItem()
{
    if(!leftPocketItem || itemDropLocked)
        return;

    DropItem(leftPocketItem);
    leftPocketItem = nullptr;

    if(auto WM = AMainGameModeBase::GetWidgetsManager())
        WM->SetInventoryFirstItem(FText::GetEmpty());
}

void APlayerBase::DropRightHandItem()
{
    if(!rightPocketItem || itemDropLocked)
        return;

    DropItem(rightPocketItem);
    rightPocketItem = nullptr;

    if(auto WM = AMainGameModeBase::GetWidgetsManager())
        WM->SetInventorySecondItem(FText::GetEmpty());
}

void APlayerBase::ShowInventory()
{
    if(inventoryLocked)
        return;

    if(auto WM = AMainGameModeBase::GetWidgetsManager())
        WM->ShowInventory();
}

void APlayerBase::ShowJournal()
{
    if(GetWorld()->IsPaused())
        return;

    if(auto WM = AMainGameModeBase::GetWidgetsManager())
        WM->ShowJournal();
}

void APlayerBase::ShowMenu()
{
    if(auto WM = AMainGameModeBase::GetWidgetsManager())
    {
        if(GetWorld()->IsPaused())
        {
            WM->HideMainMenu();
            playerController->SetShowMouseCursor(false);
            playerController->SetInputMode(FInputModeGameOnly{});
            UnlockPlayer(FPlayerLock::All());
            UGameplayStatics::SetGamePaused(GetWorld(), false);
        }
        else
        {
            WM->ShowMainMenu();
            playerController->SetShowMouseCursor(true);
            playerController->SetInputMode(FInputModeGameAndUI{});
            LockPlayer(FPlayerLock::All());
            UGameplayStatics::SetGamePaused(GetWorld(), true);
        }
    }
}

FPlayerLock FPlayerLock::All()
{
    FPlayerLock lock;
    std::memset(&lock, 0xFF, sizeof(FPlayerLock));
    return std::move(lock);
}
