// Oleg Petruny proprietary.

#include "SubwaySurfManager.h"

#include "Camera/CameraComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/SplineComponent.h"
#include "EnhancedInputComponent.h"
#include "InputMappingContext.h"

#include "CustomPlayerController.h"
#include "MainGameModeBase.h"
#include "PlayerBase.h"
#include "SubwaySurfObstacle.h"
#include "Widgets/WidgetsManager.h"

ASubwaySurfManager::ASubwaySurfManager()
    : AMinigame()
{
    static ConstructorHelpers::FObjectFinder<UInputMappingContext> asset{ TEXT("/Script/EnhancedInput.InputMappingContext'/Game/Input/Minigame/IMC_Minigame_SubwaySurf.IMC_Minigame_SubwaySurf'") };
    context = asset.Object;

    auto root = CreateDefaultSubobject<USceneComponent>(TEXT("Scene"));

    playerPos = CreateDefaultSubobject<USceneComponent>(TEXT("PlayerPos"));
    playerPos->SetupAttachment(root);

    camera = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
    camera->SetupAttachment(root);

    mannequin = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mannequin"));
    mannequin->SetupAttachment(root);
}

void ASubwaySurfManager::Start(APlayerBase* playerPawn, FMinigameEndCallback delegate)
{
    if(!playerPawn)
        return;

    AMinigame::Start(playerPawn, delegate);

    player->LockPlayer(FPlayerLock::All());
    player->SwitchToView(this);

    auto input = ACustomPlayerController::GetInput();
    for(auto& mapping : context.LoadSynchronous()->GetMappings())
    {
        if(mapping.Action->ActionDescription.EqualTo(FText::AsCultureInvariant(TEXT("Up"))))
            inputHandlers.Add(input->BindAction(mapping.Action, ETriggerEvent::Triggered, this, &ASubwaySurfManager::Up).GetHandle());
        else if(mapping.Action->ActionDescription.EqualTo(FText::AsCultureInvariant(TEXT("Down"))))
            inputHandlers.Add(input->BindAction(mapping.Action, ETriggerEvent::Triggered, this, &ASubwaySurfManager::Down).GetHandle());
        else if(mapping.Action->ActionDescription.EqualTo(FText::AsCultureInvariant(TEXT("Left"))))
            inputHandlers.Add(input->BindAction(mapping.Action, ETriggerEvent::Triggered, this, &ASubwaySurfManager::Left).GetHandle());
        else if(mapping.Action->ActionDescription.EqualTo(FText::AsCultureInvariant(TEXT("Right"))))
            inputHandlers.Add(input->BindAction(mapping.Action, ETriggerEvent::Triggered, this, &ASubwaySurfManager::Right).GetHandle());
    }

    playerPos->SetRelativeLocation(lines[1]->GetLocationAtTime(lines[1]->Duration * 0.01f, ESplineCoordinateSpace::Local));
    mannequin->SetWorldLocationAndRotation(playerPos->GetComponentLocation(), playerPos->GetComponentRotation());

    PrimaryActorTick.SetTickFunctionEnable(true);
}

void ASubwaySurfManager::End()
{
    PrimaryActorTick.SetTickFunctionEnable(false);

    for(int32 handler : inputHandlers)
        ACustomPlayerController::GetInput()->RemoveBindingByHandle(handler);

    player->UnlockPlayer(FPlayerLock::All());
    player->ReturnPlayerView();

    AMinigame::End();
}

void ASubwaySurfManager::Hit()
{
    if(ended)
        return;

    ended = true;

    if(score > 1000)
    {
        Finish();
        return;
    }

    result = EMinigameResult::Loss;
    End();
}

void ASubwaySurfManager::Finish()
{
    result = EMinigameResult::Win;
    End();
}

void ASubwaySurfManager::PostInitializeComponents()
{
    AMinigame::PostInitializeComponents();

    GetComponents<USplineComponent>(lines);
}

void ASubwaySurfManager::Tick(float deltaTime)
{
    AMinigame::Tick(deltaTime);

    time += deltaTime;

    if(time >= spawnDelay / speed)
    {
        time = 0; // (time -= spawnDelay) is not good on lags
        CreateObstacle();
    }

    for(int32 i = 0; i < obstacles.Num(); ++i)
    {
        if(obstacles[i].time > lines[obstacles[i].lineId]->Duration)
        {
            obstacles[i].actor->Destroy();
            obstacles.RemoveAt(i);
            //CreateObstacle();
            speed += 0.15;
            score += 100 * speed;
        }
        else
        {
            obstacles[i].time += deltaTime * speed;
            obstacles[i].actor->SetActorLocation(lines[obstacles[i].lineId]->GetWorldLocationAtTime(obstacles[i].time));
        }
    }

    FVector targetpos = playerPos->GetComponentLocation();
    FVector actualpos = mannequin->GetComponentLocation();
    FVector newpos{
        FMath::FInterpTo(actualpos.X, targetpos.X, deltaTime, 20),
        FMath::FInterpTo(actualpos.Y, targetpos.Y, deltaTime, 20),
        FMath::FInterpTo(actualpos.Z, targetpos.Z, deltaTime, (FMath::Abs(actualpos.Z - targetpos.Z) < 20 ? 2.5f : 5.0f) * (speed / 2))
    };
    mannequin->SetWorldLocation(newpos, false, nullptr, ETeleportType::ResetPhysics);

    if(vertical != 1 && FMath::IsNearlyEqual(actualpos.Z, targetpos.Z, 1.0))
    {
        if(vertical == 0)
            Up();
        else
            Down();
    }
}

void ASubwaySurfManager::CreateObstacle(int32 index)
{
    auto obstacleClass = obstacleClasses.CreateIterator();
    for(int32 i = 0; i < FMath::RandRange(0, obstacleClasses.Num() - 1); ++i) // std::advance not works :(
        ++obstacleClass;

    int32 lineId;
    do
    {
        lineId = FMath::RandRange(0, lines.Num() - 1);
    } while(lineId == lastSpawnedLine);
    auto spline = lines[lineId];

    FActorSpawnParameters spawnParams{};
    spawnParams.Owner = this;

    lastSpawnedLine = lineId;
    auto obstacle = GetWorld()->SpawnActor<ASubwaySurfObstacle>(*obstacleClass, spline->GetComponentLocation(), GetActorRotation(), spawnParams);
    obstacle->subwaySurf = this;

    if(index < 0)
    {
        obstacles.Add({ .actor = obstacle, .lineId = lineId, .time = 0 });
    }
    else
    {
        obstacles[index].actor->Destroy();
        obstacles[index].actor = obstacle;
        obstacles[index].lineId = lineId;
        obstacles[index].time = 0;
    }
}

void ASubwaySurfManager::Up()
{
    if(vertical == 2)
        return;

    ++vertical;

    playerPos->AddLocalOffset(FVector::UpVector * 150);
}

void ASubwaySurfManager::Down()
{
    if(vertical == 0)
        return;

    --vertical;

    playerPos->AddLocalOffset(FVector::DownVector * 150);
}

void ASubwaySurfManager::Left()
{
    if(horizontal == 0)
        return;

    --horizontal;

    playerPos->AddLocalOffset(FVector::LeftVector * 200);
}

void ASubwaySurfManager::Right()
{
    if(horizontal == 2)
        return;

    ++horizontal;

    playerPos->AddLocalOffset(FVector::RightVector * 200);
}
