// Copyright (c) Wojciech Figat. All rights reserved.

#include "GameCooker.h"
#include "PlatformTools.h"
#include "FlaxEngine.Gen.h"
#include "Engine/Scripting/ManagedCLR/MTypes.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MException.h"
#include "Engine/Scripting/Internal/MainThreadManagedInvokeAction.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/ScriptingType.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Serialization/JsonTools.h"
#include "Engine/Content/Content.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Threading/ThreadSpawner.h"
#include "Engine/Platform/FileSystem.h"
#include "Steps/ValidateStep.h"
#include "Steps/CompileScriptsStep.h"
#include "Steps/PrecompileAssembliesStep.h"
#include "Steps/DeployDataStep.h"
#include "Steps/CollectAssetsStep.h"
#include "Steps/CookAssetsStep.h"
#include "Steps/PostProcessStep.h"
#include "Engine/Platform/ConditionVariable.h"
#include "Engine/Platform/CreateProcessSettings.h"
#include "Engine/Scripting/ManagedCLR/MDomain.h"
#include "Engine/Scripting/ManagedCLR/MCore.h"
#include "Engine/Scripting/ManagedCLR/MAssembly.h"
#include "Engine/Content/JsonAsset.h"
#include "Engine/Content/AssetReference.h"
#include "Engine/Profiler/ProfilerMemory.h"
#if PLATFORM_TOOLS_WINDOWS
#include "Platform/Windows/WindowsPlatformTools.h"
#include "Engine/Platform/Windows/WindowsPlatformSettings.h"
#endif
#if PLATFORM_TOOLS_UWP
#include "Platform/UWP/UWPPlatformTools.h"
#include "Engine/Platform/UWP/UWPPlatformSettings.h"
#endif
#if PLATFORM_TOOLS_LINUX
#include "Platform/Linux/LinuxPlatformTools.h"
#include "Engine/Platform/Linux/LinuxPlatformSettings.h"
#endif
#if PLATFORM_TOOLS_PS4
#include "Platforms/PS4/Editor/PlatformTools/PS4PlatformTools.h"
#endif
#if PLATFORM_TOOLS_PS5
#include "Platforms/PS5/Editor/PlatformTools/PS5PlatformTools.h"
#endif
#if PLATFORM_TOOLS_XBOX_ONE
#include "Platforms/XboxOne/Editor/PlatformTools/XboxOnePlatformTools.h"
#endif
#if PLATFORM_TOOLS_XBOX_SCARLETT
#include "Platforms/XboxScarlett/Editor/PlatformTools/XboxScarlettPlatformTools.h"
#endif
#if PLATFORM_TOOLS_ANDROID
#include "Platform/Android/AndroidPlatformTools.h"
#endif
#if PLATFORM_TOOLS_SWITCH
#include "Platforms/Switch/Editor/PlatformTools/SwitchPlatformTools.h"
#endif
#if PLATFORM_TOOLS_MAC
#include "Platform/Mac/MacPlatformTools.h"
#include "Engine/Platform/Mac/MacPlatformSettings.h"
#endif
#if PLATFORM_TOOLS_IOS
#include "Platform/iOS/iOSPlatformTools.h"
#include "Engine/Platform/iOS/iOSPlatformSettings.h"
#endif
#if PLATFORM_TOOLS_WEB
#include "Platform/Web/WebPlatformTools.h"
#include "Engine/Platform/Web/WebPlatformSettings.h"
#endif

namespace GameCookerImpl
{
    MMethod* Internal_OnEvent = nullptr;
    MMethod* Internal_OnProgress = nullptr;
    MMethod* Internal_OnCollectAssets = nullptr;

    volatile bool IsRunning = false;
    volatile bool IsThreadRunning = false;
    int64 CancelFlag = 0;
    int64 CancelThreadFlag = 0;
    ConditionVariable ThreadCond;

    CriticalSection ProgressLocker;
    String ProgressMsg;
    float ProgressValue;

    CookingData* Data = nullptr;
    Array<GameCooker::BuildStep*> Steps;
    Dictionary<BuildPlatform, PlatformTools*> Tools;

    BuildPlatform PluginDeployPlatform;
    MAssembly* PluginDeployAssembly;
    bool PluginDeployResult;

    void CallEvent(GameCooker::EventType type);
    void ReportProgress(const String& info, float totalProgress);
    void OnCollectAssets(HashSet<Guid>& assets);
    bool Build();
    int32 ThreadFunction();

    void OnEditorAssemblyUnloading(MAssembly* assembly)
    {
        Internal_OnEvent = nullptr;
        Internal_OnProgress = nullptr;
        Internal_OnCollectAssets = nullptr;
    }
}

using namespace GameCookerImpl;

Delegate<GameCooker::EventType> GameCooker::OnEvent;
Delegate<const String&, float> GameCooker::OnProgress;
Action GameCooker::DeployFiles;
Action GameCooker::PostProcessFiles;
Action GameCooker::PackageFiles;
Delegate<HashSet<Guid>&> GameCooker::OnCollectAssets;

const Char* ToString(const BuildPlatform platform)
{
    switch (platform)
    {
    case BuildPlatform::Windows32:
        return TEXT("Windows x86");
    case BuildPlatform::Windows64:
        return TEXT("Windows x64");
    case BuildPlatform::UWPx86:
        return TEXT("Windows Store x86");
    case BuildPlatform::UWPx64:
        return TEXT("Windows Store x64");
    case BuildPlatform::XboxOne:
        return TEXT("Xbox One");
    case BuildPlatform::LinuxX64:
        return TEXT("Linux x64");
    case BuildPlatform::PS4:
        return TEXT("PlayStation 4");
    case BuildPlatform::XboxScarlett:
        return TEXT("Xbox Scarlett");
    case BuildPlatform::AndroidARM64:
        return TEXT("Android ARM64");
    case BuildPlatform::Switch:
        return TEXT("Switch");
    case BuildPlatform::PS5:
        return TEXT("PlayStation 5");
    case BuildPlatform::MacOSx64:
        return TEXT("Mac x64");
    case BuildPlatform::MacOSARM64:
        return TEXT("Mac ARM64");
    case BuildPlatform::iOSARM64:
        return TEXT("iOS ARM64");
    case BuildPlatform::WindowsARM64:
        return TEXT("Windows ARM64");
    case BuildPlatform::Web:
        return TEXT("Web");
    default:
        return TEXT("");
    }
}

const Char* ToString(const BuildConfiguration configuration)
{
    switch (configuration)
    {
    case BuildConfiguration::Debug:
        return TEXT("Debug");
    case BuildConfiguration::Development:
        return TEXT("Development");
    case BuildConfiguration::Release:
        return TEXT("Release");
    default:
        return TEXT("");
    }
}

const Char* ToString(const DotNetAOTModes mode)
{
    switch (mode)
    {
    case DotNetAOTModes::None:
        return TEXT("None");
    case DotNetAOTModes::ILC:
        return TEXT("ILC");
    case DotNetAOTModes::MonoAOTDynamic:
        return TEXT("MonoAOTDynamic");
    case DotNetAOTModes::MonoAOTStatic:
        return TEXT("MonoAOTStatic");
    case DotNetAOTModes::NoDotnet:
        return TEXT("NoDotnet");
    default:
        return TEXT("");
    }
}

bool PlatformTools::IsNativeCodeFile(CookingData& data, const String& file)
{
    const String filename = StringUtils::GetFileName(file);
    if (filename.Contains(TEXT(".CSharp")) ||
        filename.Contains(TEXT("Newtonsoft.Json")))
        return false;
    // TODO: maybe use Mono.Cecil via Flax.Build to read assembly image metadata and check if it contains C#?
    return true;
}

bool CookingData::AssetTypeStatistics::operator<(const AssetTypeStatistics& other) const
{
    if (ContentSize != other.ContentSize)
        return ContentSize > other.ContentSize;
    return Count > other.Count;
}

CookingData::CookingData(const SpawnParams& params)
    : ScriptingObject(params)
{
}

String CookingData::GetGameBinariesPath() const
{
    const Char* archDir;
    switch (Tools->GetArchitecture())
    {
    case ArchitectureType::AnyCPU:
        archDir = TEXT("AnyCPU");
        break;
    case ArchitectureType::x86:
        archDir = TEXT("x86");
        break;
    case ArchitectureType::x64:
        archDir = TEXT("x64");
        break;
    case ArchitectureType::ARM:
        archDir = TEXT("ARM");
        break;
    case ArchitectureType::ARM64:
        archDir = TEXT("ARM64");
        break;
    default:
    CRASH;
        return String::Empty;
    }

    return GetPlatformBinariesRoot() / TEXT("Game") / archDir / ::ToString(Configuration);
}

String CookingData::GetPlatformBinariesRoot() const
{
    return Globals::StartupFolder / TEXT("Source/Platforms") / Tools->GetName() / TEXT("Binaries");
}

void CookingData::GetBuildPlatformName(const Char*& platform, const Char*& architecture) const
{
    switch (Platform)
    {
    case BuildPlatform::Windows32:
        platform = TEXT("Windows");
        architecture = TEXT("x86");
        break;
    case BuildPlatform::Windows64:
        platform = TEXT("Windows");
        architecture = TEXT("x64");
        break;
    case BuildPlatform::UWPx86:
        platform = TEXT("UWP");
        architecture = TEXT("x86");
        break;
    case BuildPlatform::UWPx64:
        platform = TEXT("UWP");
        architecture = TEXT("x64");
        break;
    case BuildPlatform::XboxOne:
        platform = TEXT("XboxOne");
        architecture = TEXT("x64");
        break;
    case BuildPlatform::LinuxX64:
        platform = TEXT("Linux");
        architecture = TEXT("x64");
        break;
    case BuildPlatform::PS4:
        platform = TEXT("PS4");
        architecture = TEXT("x64");
        break;
    case BuildPlatform::XboxScarlett:
        platform = TEXT("XboxScarlett");
        architecture = TEXT("x64");
        break;
    case BuildPlatform::AndroidARM64:
        platform = TEXT("Android");
        architecture = TEXT("ARM64");
        break;
    case BuildPlatform::Switch:
        platform = TEXT("Switch");
        architecture = TEXT("ARM64");
        break;
    case BuildPlatform::PS5:
        platform = TEXT("PS5");
        architecture = TEXT("x64");
        break;
    case BuildPlatform::MacOSx64:
        platform = TEXT("Mac");
        architecture = TEXT("x64");
        break;
    case BuildPlatform::MacOSARM64:
        platform = TEXT("Mac");
        architecture = TEXT("ARM64");
        break;
    case BuildPlatform::iOSARM64:
        platform = TEXT("iOS");
        architecture = TEXT("ARM64");
        break;
    case BuildPlatform::WindowsARM64:
        platform = TEXT("Windows");
        architecture = TEXT("ARM64");
        break;
    case BuildPlatform::Web:
        platform = TEXT("Web");
        architecture = TEXT("x86");
        break;
    default:
        LOG(Fatal, "Unknown or unsupported build platform.");
    }
}

String CookingData::GetDotnetCommandArg() const
{
    int32 version = Tools->GetDotnetVersion();
    if (version == 0)
        return String::Empty;
    return String::Format(TEXT("-dotnet={}"), version);
}

void CookingData::StepProgress(const String& info, const float stepProgress) const
{
    const float singleStepProgress = 1.0f / (StepsCount + 1);
    const float totalProgress = (CurrentStepIndex + stepProgress) * singleStepProgress;
    ReportProgress(info, totalProgress);
}

void CookingData::AddRootAsset(const Guid& id)
{
    RootAssets.Add(id);
}

void CookingData::AddRootAsset(const String& path)
{
    AssetInfo info;
    if (Content::GetAssetInfo(path, info))
    {
        RootAssets.Add(info.ID);
    }
}

void CookingData::AddRootEngineAsset(const String& internalPath)
{
    const String path = Globals::EngineContentFolder / internalPath + ASSET_FILES_EXTENSION_WITH_DOT;
    AssetInfo info;
    if (Content::GetAssetInfo(path, info))
    {
        RootAssets.Add(info.ID);
    }
}

void CookingData::Error(const StringView& msg)
{
    LOG_STR(Error, msg);
}

class GameCookerService : public EngineService
{
public:

    GameCookerService()
        : EngineService(TEXT("Game Cooker"))
    {
    }

    bool Init() override;
    void Update() override;
    void Dispose() override;
};

GameCookerService GameCookerServiceInstance;

CookingData* GameCooker::GetCurrentData()
{
    return Data;
}

bool GameCooker::IsRunning()
{
    return GameCookerImpl::IsRunning;
}

bool GameCooker::IsCancelRequested()
{
    return Platform::AtomicRead(&CancelFlag) != 0;
}

PlatformTools* GameCooker::GetTools(BuildPlatform platform)
{
    PROFILE_MEM(Editor);
    PlatformTools* result = nullptr;
    if (!Tools.TryGet(platform, result))
    {
        switch (platform)
        {
#if PLATFORM_TOOLS_WINDOWS
        case BuildPlatform::Windows32:
            result = New<WindowsPlatformTools>(ArchitectureType::x86);
            break;
        case BuildPlatform::Windows64:
            result = New<WindowsPlatformTools>(ArchitectureType::x64);
            break;
        case BuildPlatform::WindowsARM64:
            result = New<WindowsPlatformTools>(ArchitectureType::ARM64);
            break;
#endif
#if PLATFORM_TOOLS_UWP
        case BuildPlatform::UWPx86:
            result = New<UWPPlatformTools>(ArchitectureType::x86);
            break;
        case BuildPlatform::UWPx64:
            result = New<UWPPlatformTools>(ArchitectureType::x64);
            break;
#endif
#if PLATFORM_TOOLS_XBOX_ONE
        case BuildPlatform::XboxOne:
            result = New<XboxOnePlatformTools>();
            break;
#endif
#if PLATFORM_TOOLS_LINUX
        case BuildPlatform::LinuxX64:
            result = New<LinuxPlatformTools>();
            break;
#endif
#if PLATFORM_TOOLS_PS4
        case BuildPlatform::PS4:
            result = New<PS4PlatformTools>();
            break;
#endif
#if PLATFORM_TOOLS_XBOX_SCARLETT
        case BuildPlatform::XboxScarlett:
            result = New<XboxScarlettPlatformTools>();
            break;
#endif
#if PLATFORM_TOOLS_ANDROID
        case BuildPlatform::AndroidARM64:
            result = New<AndroidPlatformTools>(ArchitectureType::ARM64);
            break;
#endif
#if PLATFORM_TOOLS_SWITCH
        case BuildPlatform::Switch:
            result = New<SwitchPlatformTools>();
            break;
#endif
#if PLATFORM_TOOLS_PS5
        case BuildPlatform::PS5:
            result = New<PS5PlatformTools>();
            break;
#endif
#if PLATFORM_TOOLS_MAC
        case BuildPlatform::MacOSx64:
            result = New<MacPlatformTools>(ArchitectureType::x64);
            break;
        case BuildPlatform::MacOSARM64:
            result = New<MacPlatformTools>(ArchitectureType::ARM64);
            break;
#endif
#if PLATFORM_TOOLS_IOS
        case BuildPlatform::iOSARM64:
            result = New<iOSPlatformTools>();
            break;
#endif
#if PLATFORM_TOOLS_WEB
        case BuildPlatform::Web:
            result = New<WebPlatformTools>();
            break;
#endif
        }
        Tools.Add(platform, result);
    }
    return result;
}

bool GameCooker::Build(BuildPlatform platform, BuildConfiguration configuration, const StringView& outputPath, BuildOptions options, const Array<String>& customDefines, const StringView& preset, const StringView& presetTarget)
{
    if (IsRunning())
    {
        LOG(Warning, "Cannot start a build. Already running.");
        return true;
    }
    PlatformTools* tools = GetTools(platform);
    if (tools == nullptr)
    {
        LOG(Error, "Build platform {0} is not supported.", ::ToString(platform));
        return true;
    }
    PROFILE_MEM(Editor);

    // Setup
    CancelFlag = 0;
    ProgressMsg.Clear();
    ProgressValue = 1.0f;
    Data = New<CookingData>();
    CookingData& data = *Data;
    data.Tools = tools;
    data.Platform = platform;
    data.Configuration = configuration;
    data.Options = options;
    data.Preset = preset;
    data.PresetTarget = presetTarget;
    data.CustomDefines = customDefines;
    data.OriginalOutputPath = outputPath;
    FileSystem::NormalizePath(data.OriginalOutputPath);
    data.OriginalOutputPath = FileSystem::ConvertRelativePathToAbsolute(Globals::ProjectFolder, data.OriginalOutputPath);
    data.NativeCodeOutputPath = data.ManagedCodeOutputPath = data.DataOutputPath = data.OriginalOutputPath;
    data.CacheDirectory = Globals::ProjectCacheFolder / TEXT("Cooker") / tools->GetName();
    if (!FileSystem::DirectoryExists(data.CacheDirectory))
    {
        if (FileSystem::CreateDirectory(data.CacheDirectory))
        {
            LOG(Error, "Cannot setup game building cache directory.");
            return true;
        }
    }

    // Start
    GameCookerImpl::IsRunning = true;

    // Start thread if need to
    if (!IsThreadRunning)
    {
        Function<int32()> f;
        f.Bind(ThreadFunction);
        uint32 stackSize = 4 * 1024 * 1024; // Larger stack
        const auto thread = ThreadSpawner::Start(f, GameCookerServiceInstance.Name, ThreadPriority::Highest, stackSize);
        if (thread == nullptr)
        {
            GameCookerImpl::IsRunning = false;
            LOG(Error, "Failed to start a build thread.");
            return true;
        }
    }
    else
    {
        ThreadCond.NotifyOne();
    }

    return false;
}

void GameCooker::Cancel(bool waitForEnd)
{
    if (!IsRunning())
        return;

    // Set flag
    Platform::InterlockedIncrement(&CancelFlag);

    if (waitForEnd)
    {
        LOG(Warning, "Waiting for the Game Cooker end...");

        // Wait for the end
        while (GameCookerImpl::IsRunning)
        {
            Platform::Sleep(10);
        }
    }
}

void GameCooker::GetCurrentPlatform(PlatformType& platform, BuildPlatform& buildPlatform, BuildConfiguration& buildConfiguration)
{
    platform = PLATFORM_TYPE;
#if BUILD_DEBUG
    buildConfiguration = BuildConfiguration::Debug;
#elif BUILD_DEVELOPMENT
    buildConfiguration = BuildConfiguration::Development;
#elif BUILD_RELEASE
    buildConfiguration = BuildConfiguration::Release;
#endif
    switch (PLATFORM_TYPE)
    {
    case PlatformType::Windows:
        if (PLATFORM_ARCH == ArchitectureType::x64)
            buildPlatform = BuildPlatform::Windows64;
        else if (PLATFORM_ARCH == ArchitectureType::ARM64)
            buildPlatform = BuildPlatform::WindowsARM64;
        else
            buildPlatform = BuildPlatform::Windows32;
        break;
    case PlatformType::XboxOne:
        buildPlatform = BuildPlatform::XboxOne;
        break;
    case PlatformType::UWP:
        buildPlatform = BuildPlatform::UWPx64;
        break;
    case PlatformType::Linux:
        buildPlatform = BuildPlatform::LinuxX64;
        break;
    case PlatformType::PS4:
        buildPlatform = BuildPlatform::PS4;
        break;
    case PlatformType::XboxScarlett:
        buildPlatform = BuildPlatform::XboxScarlett;
        break;
    case PlatformType::Android:
        buildPlatform = BuildPlatform::AndroidARM64;
        break;
    case PlatformType::Switch:
        buildPlatform = BuildPlatform::Switch;
        break;
    case PlatformType::PS5:
        buildPlatform = BuildPlatform::PS5;
        break;
    case PlatformType::Mac:
        buildPlatform = PLATFORM_ARCH_ARM || PLATFORM_ARCH_ARM64 ? BuildPlatform::MacOSARM64 : BuildPlatform::MacOSx64;
        break;
    case PlatformType::iOS:
        buildPlatform = BuildPlatform::iOSARM64;
        break;
    case PlatformType::Web:
        buildPlatform = BuildPlatform::Web;
        break;
    default: ;
    }
}

void GameCookerImpl::CallEvent(GameCooker::EventType type)
{
    if (Internal_OnEvent == nullptr)
    {
        auto c = GameCooker::GetStaticClass();
        if (c)
            Internal_OnEvent = c->GetMethod("Internal_OnEvent", 1);
        ASSERT(GameCookerImpl::Internal_OnEvent);
    }

    MainThreadManagedInvokeAction::ParamsBuilder params;
    params.AddParam((int32)type);
    MainThreadManagedInvokeAction::Invoke(Internal_OnEvent, params);

    GameCooker::OnEvent(type);
}

void GameCookerImpl::ReportProgress(const String& info, float totalProgress)
{
    ScopeLock lock(ProgressLocker);

    ProgressMsg = info;
    ProgressValue = totalProgress;
}

void GameCookerImpl::OnCollectAssets(HashSet<Guid>& assets)
{
    PROFILE_MEM(Editor);
    if (Internal_OnCollectAssets == nullptr)
    {
        auto c = GameCooker::GetStaticClass();
        if (c)
            Internal_OnCollectAssets = c->GetMethod("Internal_OnCollectAssets", 0);
        ASSERT(GameCookerImpl::Internal_OnCollectAssets);
    }

    MCore::Thread::Attach();
    MObject* exception = nullptr;
    auto list = (MArray*)Internal_OnCollectAssets->Invoke(nullptr, nullptr, &exception);
    if (exception)
    {
        MException ex(exception);
        ex.Log(LogType::Error, TEXT("OnCollectAssets"));
    }

    if (list)
    {
        auto ids = MUtils::ToSpan<Guid>(list);
        for (int32 i = 0; i < ids.Length(); i++)
            assets.Add(ids[i]);
    }
}

bool GameCookerImpl::Build()
{
    PROFILE_MEM(Editor);
    CookingData& data = *Data;
    LOG(Info, "Starting Game Cooker...");
    LOG(Info, "Platform: {0}, Configuration: {2}, Options: {1}", ::ToString(data.Platform), (int32)data.Options, ::ToString(data.Configuration));
    LOG(Info, "Output Path: {0}", data.OriginalOutputPath);

    // Late init feature
    if (Steps.IsEmpty())
    {
        Steps.Add(New<ValidateStep>());
        Steps.Add(New<CompileScriptsStep>());
        Steps.Add(New<DeployDataStep>());
        Steps.Add(New<PrecompileAssembliesStep>());
        Steps.Add(New<CollectAssetsStep>());
        Steps.Add(New<CookAssetsStep>());
        Steps.Add(New<PostProcessStep>());
    }

    MCore::Thread::Attach();

    // Build start
    {
        CallEvent(GameCooker::EventType::BuildStarted);
        data.Tools->OnBuildStarted(data);
        for (int32 stepIndex = 0; stepIndex < Steps.Count(); stepIndex++)
            Steps[stepIndex]->OnBuildStarted(data);
        data.InitProgress(Steps.Count());
    }

    // Execute all steps in a sequence
    bool failed = false;
    for (int32 stepIndex = 0; stepIndex < Steps.Count(); stepIndex++)
    {
        if (GameCooker::IsCancelRequested())
            break;
        if (EnumHasAnyFlags(data.Options, BuildOptions::NoCook))
            continue;
        auto step = Steps[stepIndex];
        data.NextStep();

        // Execute step
        failed = step->Perform(data);
        if (failed)
            break;
    }

    // Process result
    if (GameCooker::IsCancelRequested())
    {
        LOG(Warning, "Game building cancelled!");
        failed = true;
    }
    else if (failed)
    {
        LOG(Error, "Game building failed!");
    }
    else
    {
        LOG(Info, "Game building done!");

        if (EnumHasAnyFlags(data.Options, BuildOptions::ShowOutput))
        {
            FileSystem::ShowFileExplorer(data.OriginalOutputPath);
        }

        if (EnumHasAnyFlags(data.Options, BuildOptions::AutoRun))
        {
            String executableFile, commandLineFormat, workingDir;
            data.Tools->OnRun(data, executableFile, commandLineFormat, workingDir);
            if (executableFile.HasChars())
            {
                const String gameArgs; // TODO: pass custom game run args from Editor? eg. starting map? or client info?
                const String commandLine = commandLineFormat.HasChars() ? String::Format(*commandLineFormat, gameArgs) : gameArgs;
                if (workingDir.IsEmpty())
                    workingDir = data.NativeCodeOutputPath;
                CreateProcessSettings procSettings;
                procSettings.FileName = executableFile;
                procSettings.Arguments = commandLine;
                procSettings.WorkingDirectory = workingDir;
                procSettings.HiddenWindow = false;
                procSettings.WaitForEnd = false;
                procSettings.LogOutput = false;
                procSettings.ShellExecute = true;
                Platform::CreateProcess(procSettings);
            }
            else
            {
                LOG(Warning, "Missing executable to run or platform doesn't support build&run.");
            }
        }
    }
    IsRunning = false;
    CancelFlag = 0;
    {
        // Build end
        for (int32 stepIndex = 0; stepIndex < Steps.Count(); stepIndex++)
            Steps[stepIndex]->OnBuildEnded(data, failed);
        data.Tools->OnBuildEnded(data, failed);
        CallEvent(failed ? GameCooker::EventType::BuildFailed : GameCooker::EventType::BuildDone);
    }
    Delete(Data);
    Data = nullptr;

    return failed;
}

int32 GameCookerImpl::ThreadFunction()
{
    IsThreadRunning = true;

    CriticalSection mutex;
    while (Platform::AtomicRead(&CancelThreadFlag) == 0)
    {
        if (IsRunning)
        {
            Build();
        }

        ThreadCond.Wait(mutex);
    }

    IsThreadRunning = false;
    return 0;
}

bool GameCookerService::Init()
{
    PROFILE_MEM(Editor);

    auto editorAssembly = ((NativeBinaryModule*)GetBinaryModuleFlaxEngine())->Assembly;
    editorAssembly->Unloading.Bind(OnEditorAssemblyUnloading);
    GameCooker::OnCollectAssets.Bind(OnCollectAssets);

    return false;
}

void GameCookerService::Update()
{
    if (IsRunning)
    {
        PROFILE_MEM(Editor);
        ScopeLock lock(ProgressLocker);

        if (ProgressMsg.HasChars())
        {
            if (Internal_OnProgress == nullptr)
            {
                auto c = GameCooker::GetStaticClass();
                if (c)
                    Internal_OnProgress = c->GetMethod("Internal_OnProgress", 2);
                ASSERT(GameCookerImpl::Internal_OnProgress);
            }

            MainThreadManagedInvokeAction::ParamsBuilder params;
            params.AddParam(ProgressMsg, Scripting::GetScriptsDomain());
            params.AddParam(ProgressValue);
            MainThreadManagedInvokeAction::Invoke(Internal_OnProgress, params);
            GameCooker::OnProgress(ProgressMsg, ProgressValue);

            ProgressMsg.Clear();
            ProgressValue = 1.0f;
        }
    }
}

void GameCookerService::Dispose()
{
    // Always stop on exit
    GameCooker::Cancel(true);

    // End thread
    if (IsThreadRunning)
    {
        LOG(Warning, "Waiting for the Game Cooker thread end...");

        Platform::AtomicStore(&CancelThreadFlag, 1);
        ThreadCond.NotifyOne();
        while (IsThreadRunning)
        {
            Platform::Sleep(1);
        }
    }

    // Cleanup
    Steps.ClearDelete();
    Tools.ClearDelete();
}
