// Oleg Petruny proprietary.

#include "DialogueManager.h"

#include "Components/AudioComponent.h"
#include "Engine/DataTable.h"
#include "EnhancedInputComponent.h"
#include "InputMappingContext.h"
#include "Kismet/GameplayStatics.h"
#include "Kismet/KismetMathLibrary.h"
#include "Sound/SoundWave.h"

#include "CustomPlayerController.h"
#include "MainGameModeBase.h"
#include "PlayerBase.h"
#include "Widgets/DialogueSkipWidget.h"
#include "Widgets/InputAnimatedWidgetInterface.h"
#include "Widgets/WidgetsManager.h"

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

void UDialogueManager::PlayDialogue(FDialogueEnqueProperties properties, FDialogueEndCallback endCallback)
{
    if(!properties.dialogue)
        return;

    if(properties.playMode == EDialoguePlayMode::Random)
    {
        TArray<FName> rows = properties.dialogue.LoadSynchronous()->GetRowNames();
        properties.rowName = rows[FMath::RandRange(0, rows.Num() - 1)];
    }

    FDialogueRow* row = reinterpret_cast<FDialogueRow*>(properties.dialogue.LoadSynchronous()->FindRowUnchecked(properties.rowName));

    if(!row)
        return;

    FTimerHandle timer;
    int32 timerId;
    timersLock.Lock();
    timerId = timers.Num();
    timers.Add(timer);
    timersLock.Unlock();

    UGameplayStatics::PlaySound2D(this, row->wave.LoadSynchronous());
    if(auto WM = AMainGameModeBase::GetWidgetsManager())
        WM->ShowDialogueWidget(*row);

    auto func = [properties,
        timerId,
        timersLock = &timersLock,
        timers = &timers,
        endCallback]()
        {
            FDialogueRow* row = reinterpret_cast<FDialogueRow*>(properties.dialogue.LoadSynchronous()->FindRowUnchecked(properties.rowName));
            if(auto WM = AMainGameModeBase::GetWidgetsManager())
                WM->HideDialogueWidget(*row);

            timersLock->Lock();
            timers->RemoveAt(timerId);
            timersLock->Unlock();

            endCallback.Execute();
        };

    float duration = row->wave ? row->wave->GetDuration() : row->duration;
    GetWorld()->GetTimerManager().SetTimer(timer, func, duration, false);
}

void UDialogueManager::EnqueDialogue(FDialogueEnqueProperties properties, FDialogueEndCallback endCallback)
{
    if(!properties.dialogue.LoadSynchronous())
        return;

    FScopeLock lock1(&dialoguesLock);
    FScopeLock lock2(&callbacksLock);

    OnFirstDialogueInit();

    nextDialogues.Enqueue(properties);
    endCallbacks.Enqueue(endCallback);

    PlayNextDialogue();
}

void UDialogueManager::PlayNextDialogue()
{
    dialoguesLock.Lock();

    auto properties = nextDialogues.Peek();

    TArray<FName> rows = properties->dialogue.LoadSynchronous()->GetRowNames();
    if(rows.Num() == 0)
    {
        FDialogueEndCallback callback;
        if(endCallbacks.Dequeue(callback))
            callback.Execute();
        nextDialogues.Pop();
        return;
    }

    if(properties->playMode == EDialoguePlayMode::Random)
    {
        properties->rowName = rows[FMath::RandRange(0, rows.Num() - 1)];
    }

    FDialogueRow* row = reinterpret_cast<FDialogueRow*>(properties->dialogue.LoadSynchronous()->FindRowUnchecked(properties->rowName));

    if(!row)
    {
        FDialogueEndCallback callback;
        endCallbacks.Dequeue(callback);
        callback.ExecuteIfBound();
        nextDialogues.Pop();
        return;
    }

    if(properties->playMode == EDialoguePlayMode::Sequential
        && !properties->rowName.ToString().IsNumeric())
    {
        nextDialogues.Pop();
        FDialogueEndCallback callback;
        if(endCallbacks.Dequeue(callback))
            callback.Execute();
        return;
    }

    if(row->wave.LoadSynchronous())
        leadDialogueAudio = UGameplayStatics::SpawnSound2D(this, row->wave.LoadSynchronous());
    else
        leadDialogueAudio = nullptr;

    if(auto WM = AMainGameModeBase::GetWidgetsManager())
        WM->ShowDialogueWidget(*row);

    FTimerHandle timer;
    int32 timerId;

    const float duration = row->wave ? row->wave->GetDuration() : row->duration;
    GetWorld()->GetTimerManager().SetTimer(timer, [&]() { OnDialogueEnd(); }, duration, false);

    timersLock.Lock();
    timerId = timers.Num();
    timers.Add(timer);
    timersLock.Unlock();

    leadDialogueTimerId = timerId;
    leadDialogueProperties = properties;

    dialoguesLock.Unlock();
}

void UDialogueManager::OnFirstDialogueInit() // most first dialogue, so widgets and binds don't exist
{
    if(!endCallbacks.IsEmpty())
        return;

    if(auto PC = UGameplayStatics::GetPlayerController(GetWorld(), 0))
    {
        lastPlayer = Cast<APlayerBase>(PC->GetPawn());
        if(lastPlayer)
        {
            auto& mapping = context.LoadSynchronous()->GetMapping(0);
            inputHandler = ACustomPlayerController::GetInput()->BindAction(mapping.Action, ETriggerEvent::Triggered, this, &UDialogueManager::OnInputPress).GetHandle();

            if(auto WM = AMainGameModeBase::GetWidgetsManager())
            {
                static FDialogueSkipDelegate skipDialogueDelegate;
                if(!skipDialogueDelegate.IsBound())
                {
                    skipDialogueDelegate.BindDynamic(this, &UDialogueManager::SkipDialogue);
                    WM->SetInputDialogueWidget(mapping.Key, mapping.Action->ActionDescription, skipDialogueDelegate);
                }
            }
        }
    }
}

void UDialogueManager::SkipDialogue()
{
    if(timers.Num() == 0 || leadDialogueTimerId < 0)
        return;

    timersLock.Lock();
    GetWorld()->GetTimerManager().ClearTimer(timers[leadDialogueTimerId]);
    timers.RemoveAt(leadDialogueTimerId);
    leadDialogueTimerId = -1;
    timersLock.Unlock();

    if(leadDialogueAudio)
        leadDialogueAudio->Stop();

    OnDialogueEnd();
}

void UDialogueManager::ClearQueue()
{
    if(!nextDialogues.IsEmpty())
        nextDialogues.Empty();
    if(!endCallbacks.IsEmpty())
        endCallbacks.Empty();
}

void UDialogueManager::LockCallback(bool lock)
{
    lockCallback = lock;
}

void UDialogueManager::BeginDestroy()
{
    if(auto world = GetWorld())
    {
        timersLock.Lock();
        for(auto& timer : timers)
            world->GetTimerManager().ClearTimer(timer);
        timersLock.Unlock();
    }

    UObject::BeginDestroy();
}

void UDialogueManager::OnDialogueEnd()
{
    dialoguesLock.Lock();

    FDialogueRow* row = reinterpret_cast<FDialogueRow*>(leadDialogueProperties->dialogue.LoadSynchronous()->FindRowUnchecked(leadDialogueProperties->rowName));

    if(auto WM = AMainGameModeBase::GetWidgetsManager())
        WM->HideDialogueWidget(*row);

    if(leadDialogueProperties->playMode == EDialoguePlayMode::Sequential
        && leadDialogueProperties->rowName.ToString().IsNumeric())
    {
        leadDialogueProperties->rowName = FName(FString::FromInt(FCString::Atoi(*(leadDialogueProperties->rowName.ToString())) + 1));
    }

    dialoguesLock.Unlock();

    if(!endCallbacks.IsEmpty())
        PlayNextDialogue();

    dialoguesLock.Lock();
    if(endCallbacks.IsEmpty() && lastPlayer)
    {
        ACustomPlayerController::GetInput()->RemoveBindingByHandle(inputHandler);
        lastPlayer = nullptr;
    }
    dialoguesLock.Unlock();
}

void UDialogueManager::OnInputPress()
{
    if(auto WM = AMainGameModeBase::GetWidgetsManager())
        WM->AnimateDialogueWidget(EInputAnimatedWidgetAnimation::Click);
}
