#include "RiderGameControl.hpp"


#include "IRiderLink.hpp"

#include "Model/Library/UE4Library/PlayState.Pregenerated.h"
#include "Model/Library/UE4Library/RequestFailed.Pregenerated.h"
#include "Model/Library/UE4Library/RequestSucceed.Pregenerated.h"
#include "RdEditorModel/RdEditorModel.Pregenerated.h"

#include "Async/Async.h"
#include "Editor/UnrealEdEngine.h"
#include "Framework/Application/SlateApplication.h"
#include "Kismet2/DebuggerCommands.h"
#include "LevelEditor.h"
#include "LevelEditorActions.h"
#include "Misc/FeedbackContext.h"
#include "Modules/ModuleManager.h"
#include "Settings/LevelEditorPlaySettings.h"
#include "Editor.h"

#include "Runtime/Launch/Resources/Version.h"
#if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 23
#include "ILevelViewport.h"
#include "LevelEditorViewport.h"
#else
#include "IAssetViewport.h"
#include "EditorViewportClient.h"
#endif

#define LOCTEXT_NAMESPACE "RiderGameControl"

DEFINE_LOG_CATEGORY(FLogRiderGameControlModule);

IMPLEMENT_MODULE(FRiderGameControlModule, RiderGameControl);

extern UNREALED_API class UUnrealEdEngine* GUnrealEd;

static int NumberOfPlayers(int Mode) { return (Mode & 3) + 1; }

static bool SpawnAtPlayerStart(int Mode) { return (Mode & 4) != 0; }

static bool DedicatedServer(int Mode) { return (Mode & 8) != 0; }

enum class Compile
{
    Yes,
    No
};

static Compile CompileBeforeRun(int Mode) { return (Mode & 128) != 0 ? Compile::Yes : Compile::No; }

static EPlayModeType PlayModeFromInt(int ModeNumber)
{
    switch (ModeNumber)
    {
    default: break;
    case 1: return PlayMode_InMobilePreview;
    case 2: return PlayMode_InEditorFloating;
    case 3: return PlayMode_InVR;
    case 4: return PlayMode_InNewProcess;
    case 5: return PlayMode_Simulate;
    case 6: return PlayMode_InVulkanPreview;
    }
    return PlayMode_InViewPort;
}

static int PlayModeToInt(EPlayModeType modeType)
{
    switch (modeType)
    {
    default: break;
    case PlayMode_InTargetedMobilePreview:
    case PlayMode_InMobilePreview:
        return 1;
    case PlayMode_InEditorFloating: return 2;
    case PlayMode_InVR: return 3;
    case PlayMode_InNewProcess: return 4;
    case PlayMode_Simulate: return 5;
    case PlayMode_InVulkanPreview: return 6;
    }
    return 0;
}

FSlateApplication* SlateApplication = nullptr;

struct FPlaySettings
{
    EPlayModeType PlayMode;
    int32 NumberOfClients;
    bool bNetDedicated;
    bool bSpawnAtPlayerStart;

    static FPlaySettings UnpackFromMode(int32_t mode)
    {
        FPlaySettings settings = {
            PlayModeFromInt((mode & (16 + 32 + 64)) >> 4),
            NumberOfPlayers(mode),
            DedicatedServer(mode),
            SpawnAtPlayerStart(mode),
        };
        return settings;
    }

    static int32_t PackToMode(const FPlaySettings& settings)
    {
        return (settings.NumberOfClients - 1) +
            (settings.bSpawnAtPlayerStart ? (1 << 2) : 0) +
            (settings.bNetDedicated ? (1 << 3) : 0) +
            (PlayModeToInt(settings.PlayMode) << 4);
    }
};


static FPlaySettings RetrieveSettings(const ULevelEditorPlaySettings* PlayInSettings)
{
    check(PlayInSettings);

    FPlaySettings settings;
    settings.PlayMode = PlayInSettings->LastExecutedPlayModeType;
    PlayInSettings->GetPlayNumberOfClients(settings.NumberOfClients);
#if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 24
    PlayInSettings->GetPlayNetDedicated(settings.bNetDedicated);
#else
    settings.bNetDedicated = PlayInSettings->bLaunchSeparateServer;
#endif
    settings.bSpawnAtPlayerStart = PlayInSettings->LastExecutedPlayModeLocation == PlayLocation_DefaultPlayerStart;

    return settings;
}

static void UpdateSettings(ULevelEditorPlaySettings* PlayInSettings, const FPlaySettings& settings)
{
    check(PlayInSettings);
    
    PlayInSettings->SetPlayNumberOfClients(settings.NumberOfClients);
#if ENGINE_MAJOR_VERSION == 4 && ENGINE_MINOR_VERSION <= 24
    PlayInSettings->SetPlayNetDedicated(settings.bNetDedicated);
#else
    PlayInSettings->bLaunchSeparateServer = settings.bNetDedicated;
#endif
    PlayInSettings->LastExecutedPlayModeLocation =
        settings.bSpawnAtPlayerStart
            ? PlayLocation_DefaultPlayerStart
            : PlayLocation_CurrentCameraLocation;
    PlayInSettings->LastExecutedPlayModeType = settings.PlayMode;

    PlayInSettings->PostEditChange();
    PlayInSettings->SaveConfig();
}


struct FCachedCommandInfo
{
    FName CommandName;
    TSharedPtr<FUICommandInfo> Command;
};

class FRiderGameControlActionsCache
{
public:
    FRiderGameControlActionsCache();
    ~FRiderGameControlActionsCache();

private:
    void UpdatePlayWorldCommandsCache();

public:
    FCachedCommandInfo PlayModeCommands[PlayMode_Count] = {
        {TEXT("PlayInViewport")},
        {TEXT("PlayInEditorFloating")},
        {TEXT("PlayInMobilePreview")},
        {FName()},
        {TEXT("PlayInVulkanPreview")},
        {TEXT("PlayInNewProcess")},
        {TEXT("PlayInVR")},
        {TEXT("Simulate")},
    };
    FCachedCommandInfo ResumePlaySession = {TEXT("ResumePlaySession")};
    FCachedCommandInfo PausePlaySession = {TEXT("PausePlaySession")};
    FCachedCommandInfo StopPlaySession = {TEXT("StopPlaySession")};
    FCachedCommandInfo SingleFrameAdvance = {TEXT("SingleFrameAdvance")};

private:
    FDelegateHandle CommandsChangedHandle;
};

FRiderGameControlActionsCache::FRiderGameControlActionsCache()
{
    const FName PlayWorldContextName = FName("PlayWorld");

    TSharedPtr<FBindingContext> PlayWorldContext = FInputBindingManager::Get().GetContextByName(PlayWorldContextName);
    if (PlayWorldContext.IsValid())
    {
        UpdatePlayWorldCommandsCache();
    }

    CommandsChangedHandle = FBindingContext::CommandsChanged.AddLambda(
        [this, PlayWorldContextName](const FBindingContext& Ctx)
        {
            if (Ctx.GetContextName() == PlayWorldContextName)
            {
                UpdatePlayWorldCommandsCache();
            }
        }
    );
}

FRiderGameControlActionsCache::~FRiderGameControlActionsCache()
{
    FBindingContext::CommandsChanged.Remove(CommandsChangedHandle);
}

void FRiderGameControlActionsCache::UpdatePlayWorldCommandsCache()
{
    FInputBindingManager& BindingManager = FInputBindingManager::Get();
    auto CacheCommand = [&] (FCachedCommandInfo &Cmd, const FName &ContextName)
    {
        Cmd.Command = BindingManager.FindCommandInContext(ContextName, Cmd.CommandName);
    };

    const FName PlayWorldContextName = FName("PlayWorld");
    for (FCachedCommandInfo& PlayModeCommand : PlayModeCommands)
    {
        if (PlayModeCommands->CommandName.IsNone()) continue;
        CacheCommand(PlayModeCommand, PlayWorldContextName);
    }
    CacheCommand(ResumePlaySession, PlayWorldContextName);
    CacheCommand(PausePlaySession, PlayWorldContextName);
    CacheCommand(StopPlaySession, PlayWorldContextName);
    CacheCommand(SingleFrameAdvance, PlayWorldContextName);
}


class FRiderGameControl
{
public:
    FRiderGameControl(rd::Lifetime Lifetime, JetBrains::EditorPlugin::RdEditorModel const &Model, FRiderGameControlActionsCache& ActionsCache);
    ~FRiderGameControl();
private:
    void RequestPlayWorldCommand(const FCachedCommandInfo& CommandInfo, int RequestID);

    void SendRequestSucceed(int RequestID);
    void SendRequestFailed(int RequestID, JetBrains::EditorPlugin::NotificationType Type, const FString& Message);

    void ScheduleModelAction(TFunction<void(JetBrains::EditorPlugin::RdEditorModel const&)> Action);

private:
    FRiderGameControlActionsCache& Actions;
    JetBrains::EditorPlugin::RdEditorModel const &Model;

    int32_t playMode;

    FDelegateHandle BeginPIEHandle;
    FDelegateHandle EndPIEHandle;
    FDelegateHandle PausePIEHandle;
    FDelegateHandle ResumePIEHandle;
    FDelegateHandle SingleStepPIEHandle;
    FDelegateHandle OnObjectPropertyChangedHandle;
};


void FRiderGameControl::SendRequestSucceed(int RequestID)
{
    using namespace JetBrains::EditorPlugin;
    ScheduleModelAction([=](RdEditorModel const& EditorModel)
    {
        EditorModel.get_notificationReplyFromEditor().fire(RequestSucceed(RequestID));
    });
}

void FRiderGameControl::SendRequestFailed(int RequestID, JetBrains::EditorPlugin::NotificationType Type,
                                          const FString& Message)
{
    using namespace JetBrains::EditorPlugin;
    ScheduleModelAction([=](RdEditorModel const& EditorModel)
    {
        EditorModel.get_notificationReplyFromEditor().fire(RequestFailed(Type, Message, RequestID));
    });
}

void FRiderGameControl::RequestPlayWorldCommand(const FCachedCommandInfo& CommandInfo, int RequestID)
{
    using namespace JetBrains::EditorPlugin;
    if (!CommandInfo.Command.IsValid())
    {
        const FString Message = FString::Format(TEXT("Command '{0}' was not executed.\nCommand was not registered in Unreal Engine"),
                                                {CommandInfo.CommandName.ToString()});
        SendRequestFailed(RequestID, NotificationType::Error, Message);
        return;
    }
    AsyncTask(ENamedThreads::GameThread, [this, RequestID, CommandInfo]()
    {
        if (FPlayWorldCommands::GlobalPlayWorldActions->TryExecuteAction(CommandInfo.Command.ToSharedRef()))
        {
            SendRequestSucceed(RequestID);
        }
        else
        {
            const FString Message = FString::Format(TEXT("Command '{0}' was not executed.\nRejected by Unreal Engine"),
                                                    {CommandInfo.CommandName.ToString()});
            SendRequestFailed(RequestID, NotificationType::Message, Message);
        }
    });
}

void FRiderGameControl::ScheduleModelAction(TFunction<void(JetBrains::EditorPlugin::RdEditorModel const&)> Action)
{
    IRiderLinkModule& RiderLinkModule = IRiderLinkModule::Get();
    RiderLinkModule.QueueAction([Action, this]()
    {
        Action(Model);
    });
}

FRiderGameControl::FRiderGameControl(rd::Lifetime Lifetime, JetBrains::EditorPlugin::RdEditorModel const &Model, FRiderGameControlActionsCache& ActionsCache) :
    Actions(ActionsCache), Model(Model)
{
    using namespace JetBrains::EditorPlugin;
    
    // Subscribe to Editor events
    Lifetime->bracket(
        [this]()
        {
            BeginPIEHandle = FEditorDelegates::BeginPIE.AddLambda([this](const bool)
            {
                ScheduleModelAction([](RdEditorModel const& model)
                {
                    model.get_playStateFromEditor().fire(PlayState::Play);
                });
            });
            EndPIEHandle = FEditorDelegates::EndPIE.AddLambda([this](const bool)
            {
                ScheduleModelAction([](RdEditorModel const& model)
                {
                    model.get_playStateFromEditor().fire(PlayState::Idle);
                });
            });
            PausePIEHandle = FEditorDelegates::PausePIE.AddLambda([this](const bool)
            {
                ScheduleModelAction([](RdEditorModel const& model)
                {
                    model.get_playStateFromEditor().fire(PlayState::Pause);
                });
            });
            ResumePIEHandle = FEditorDelegates::ResumePIE.AddLambda([this](const bool)
            {
                ScheduleModelAction([](RdEditorModel const& model)
                {
                    model.get_playStateFromEditor().fire(PlayState::Play);
                });
            });
            SingleStepPIEHandle = FEditorDelegates::SingleStepPIE.AddLambda([this](const bool)
            {
                ScheduleModelAction([](RdEditorModel const& model)
                {
                    model.get_playStateFromEditor().fire(PlayState::Play);
                    model.get_playStateFromEditor().fire(PlayState::Pause);
                });
            });

            OnObjectPropertyChangedHandle = FCoreUObjectDelegates::OnObjectPropertyChanged.AddLambda(
                [this](UObject* obj, FPropertyChangedEvent& ev)
                {
                    ULevelEditorPlaySettings* PlayInSettings = GetMutableDefault<ULevelEditorPlaySettings>();
                    if (!PlayInSettings || obj != PlayInSettings) return;

                    const FPlaySettings Settings = RetrieveSettings(PlayInSettings);
                    int PlayModeNew = FPlaySettings::PackToMode(Settings);
                    if (PlayModeNew == playMode) return;

                    playMode = PlayModeNew;
                    ScheduleModelAction([PlayModeNew](RdEditorModel const& Model)
                    {
                        Model.get_playModeFromEditor().fire(PlayModeNew);
                    });
                }
            );
        },
        [this]()
        {
            FCoreUObjectDelegates::OnObjectPropertyChanged.Remove(OnObjectPropertyChangedHandle);
            FEditorDelegates::SingleStepPIE.Remove(SingleStepPIEHandle);
            FEditorDelegates::ResumePIE.Remove(ResumePIEHandle);
            FEditorDelegates::PausePIE.Remove(PausePIEHandle);
            FEditorDelegates::EndPIE.Remove(EndPIEHandle);
            FEditorDelegates::BeginPIE.Remove(BeginPIEHandle);
        }
    );

    // Subscribe to model
    ScheduleModelAction([Lifetime, this](RdEditorModel const& Model)
    {
        Model.get_requestPlayFromRider()
             .advise(Lifetime, [this](int requestID)
                     {
                         const ULevelEditorPlaySettings* PlayInSettings
                             = GetDefault<ULevelEditorPlaySettings>();
                         check(PlayInSettings);
                         const EPlayModeType PlayMode = PlayInSettings->LastExecutedPlayModeType;

                         RequestPlayWorldCommand(Actions.PlayModeCommands[PlayMode], requestID);
                     }
             );
        Model.get_requestPauseFromRider()
             .advise(Lifetime, [this](int requestID)
                     {
                         RequestPlayWorldCommand(Actions.PausePlaySession, requestID);
                     }
             );
        Model.get_requestResumeFromRider()
             .advise(Lifetime, [this](int requestID)
                     {
                         RequestPlayWorldCommand(Actions.ResumePlaySession, requestID);
                     }
             );
        Model.get_requestStopFromRider()
             .advise(Lifetime, [this](int requestID)
                     {
                         RequestPlayWorldCommand(Actions.StopPlaySession, requestID);
                     }
             );
        Model.get_requestFrameSkipFromRider()
             .advise(Lifetime, [this](int requestID)
                     {
                         RequestPlayWorldCommand(Actions.SingleFrameAdvance, requestID);
                     }
             );

        Model.get_playModeFromRider()
             .advise(Lifetime, [this](int32_t mode)
                     {
                         ULevelEditorPlaySettings* PlayInSettings
                             = GetMutableDefault<ULevelEditorPlaySettings>();
                         check(PlayInSettings);
                         const FPlaySettings NewSettings = FPlaySettings::UnpackFromMode(mode);
                         UpdateSettings(PlayInSettings, NewSettings);
                     }
             );
    });

    // Initial sync.
    const ULevelEditorPlaySettings* PlayInSettings = GetDefault<ULevelEditorPlaySettings>();
    check(PlayInSettings);
    const FPlaySettings Settings = RetrieveSettings(PlayInSettings);
    playMode = FPlaySettings::PackToMode(Settings);

    ScheduleModelAction([lambdaPlayMode=playMode](RdEditorModel const &Model)
    {
        Model.get_playModeFromEditor().fire(lambdaPlayMode);
    });

    // After all initialization finished/scheduled - mark that module was initialized
    Lifetime->bracket(
        [this]()
        {
            ScheduleModelAction([](RdEditorModel const& Model)
            {
                Model.get_isGameControlModuleInitialized().set(true);
            });
        },
        [this]()
        {
            ScheduleModelAction([](RdEditorModel const& Model)
            {
                Model.get_isGameControlModuleInitialized().set(false);
            });
        }
    );
}

FRiderGameControl::~FRiderGameControl()
{
}


void FRiderGameControlModule::StartupModule()
{
    using namespace JetBrains::EditorPlugin;
    
    UE_LOG(FLogRiderGameControlModule, Verbose, TEXT("STARTUP START"));

    // Actions cache is not related to connection and its lifetimes
    ActionsCache = MakeUnique<FRiderGameControlActionsCache>();

    IRiderLinkModule& RiderLinkModule = IRiderLinkModule::Get();
    ModuleLifetimeDefinition = RiderLinkModule.CreateNestedLifetimeDefinition();
    rd::Lifetime ModuleLifetime = ModuleLifetimeDefinition.lifetime;

    RiderLinkModule.ViewModel(
        ModuleLifetime,
        [&](rd::Lifetime ModelLifetime, RdEditorModel const& Model)
        {
            ModelLifetime->add_action([&]() { GameControl.Reset(); });
            GameControl = MakeUnique<FRiderGameControl>(ModelLifetime, Model, *ActionsCache);
        }
    );

    UE_LOG(FLogRiderGameControlModule, Verbose, TEXT("STARTUP FINISH"));
}

void FRiderGameControlModule::ShutdownModule()
{
    UE_LOG(FLogRiderGameControlModule, Verbose, TEXT("SHUTDOWN START"));
    ModuleLifetimeDefinition.terminate();
    ActionsCache.Reset();
    UE_LOG(FLogRiderGameControlModule, Verbose, TEXT("SHUTDOWN FINISH"));
}

#undef LOCTEXT_NAMESPACE