// 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 "CustomGameInstanceBase.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'") };
    _inputContext = asset.Object;
    if(auto world = GetWorld())
    {
        if(auto GI = Cast<UCustomGameInstanceBase>(world->GetGameInstance()))
        {
            GI->inputContexts.Add(_inputContext);
        }
    }
}

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 = properties,
        timerId = timerId,
        _timersLock = &_timersLock,
        _timers = &_timers,
        endCallback = 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);

    if(_endCallbacks.IsEmpty()) // most first dialogue, so widgets and binds don't exist
    {
        if(auto PC = UGameplayStatics::GetPlayerController(GetWorld(), 0))
        {
            _lastPlayer = Cast<APlayerBase>(PC->GetPawn());
            if(_lastPlayer)
            {
                auto& mapping = _inputContext.LoadSynchronous()->GetMapping(0);
                _inputHandler = _lastPlayer->inputComponent->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);
                    }
                }
            }
        }
    }

    _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.Execute();
        _nextDialogues.Pop();
        return;
    }

    if(properties->playMode == EDialoguePlayMode::Sequential)
    {
        if(!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::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)
    {
        if(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)
    {
        _lastPlayer->inputComponent->RemoveBindingByHandle(_inputHandler);
        _lastPlayer = nullptr;
    }
    _dialoguesLock.Unlock();
}

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