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

#include "PrecompileAssembliesStep.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/File.h"
#include "Engine/Core/Config/BuildSettings.h"
#include "Engine/Engine/Globals.h"
#include "Editor/Scripting/ScriptsBuilder.h"
#include "Editor/Cooker/PlatformTools.h"
#include "Editor/Utilities/EditorUtilities.h"

void PrecompileAssembliesStep::OnBuildStarted(CookingData& data)
{
    const DotNetAOTModes aotMode = data.Tools->UseAOT();
    if (aotMode == DotNetAOTModes::None || 
        aotMode == DotNetAOTModes::NoDotnet || 
        EnumHasAllFlags(data.Options, BuildOptions::NoCook))
        return;
    const auto& buildSettings = *BuildSettings::Get();

    // Redirect C# assemblies to intermediate cooking directory (processed by ILC)
    data.ManagedCodeOutputPath = data.CacheDirectory / TEXT("AOTAssemblies");

    // Reset any AOT cache from previous run if the AOT mode has changed (eg. Mono AOT -> ILC on Desktop)
    const String aotModeCacheFilePath = data.ManagedCodeOutputPath / TEXT("AOTMode.txt");
    String aotModeCacheValue = String::Format(TEXT("{};{};{};{}"),
                                              (int32)aotMode,
                                              (int32)data.Configuration,
                                              (int32)buildSettings.SkipUnusedDotnetLibsPackaging,
                                              FileSystem::GetFileLastEditTime(ScriptsBuilder::GetBuildToolPath()).Ticks);
    for (const String& define : data.CustomDefines)
        aotModeCacheValue += define;
    if (FileSystem::DirectoryExists(data.ManagedCodeOutputPath))
    {
        String cachedData;
        File::ReadAllText(aotModeCacheFilePath, cachedData);
        if (cachedData != aotModeCacheValue)
        {
            LOG(Info, "AOT cache invalidation");
            FileSystem::DeleteDirectory(data.ManagedCodeOutputPath); // Remove AOT cache
            FileSystem::DeleteDirectory(data.DataOutputPath / TEXT("Dotnet")); // Remove deployed Dotnet libs (be sure to remove any leftovers from previous build)
        }
    }
    if (!FileSystem::DirectoryExists(data.ManagedCodeOutputPath))
    {
        FileSystem::CreateDirectory(data.ManagedCodeOutputPath);
        File::WriteAllText(aotModeCacheFilePath, aotModeCacheValue, Encoding::ANSI);
    }
}

bool PrecompileAssembliesStep::Perform(CookingData& data)
{
    const DotNetAOTModes aotMode = data.Tools->UseAOT();
    if (aotMode == DotNetAOTModes::None ||
        aotMode == DotNetAOTModes::NoDotnet)
        return false;
    const auto& buildSettings = *BuildSettings::Get();
    if (buildSettings.SkipDotnetPackaging && data.Tools->UseSystemDotnet())
        return false;
    LOG(Info, "Using AOT...");
    const String infoMsg = TEXT("Running AOT");
    data.StepProgress(infoMsg, 0);

    // Override Newtonsoft.Json with AOT-version (one that doesn't use System.Reflection.Emit)
    // TODO: remove it since EngineModule does properly reference AOT lib now
    EditorUtilities::CopyFileIfNewer(data.ManagedCodeOutputPath / TEXT("Newtonsoft.Json.dll"), Globals::StartupFolder / TEXT("Source/Platforms/DotNet/AOT/Newtonsoft.Json.dll"));
    FileSystem::DeleteFile(data.ManagedCodeOutputPath / TEXT("Newtonsoft.Json.xml"));
    FileSystem::DeleteFile(data.ManagedCodeOutputPath / TEXT("Newtonsoft.Json.pdb"));

    // Run AOT by Flax.Build (see DotNetAOT)
    const Char *platform, *architecture, *configuration = ::ToString(data.Configuration);
    data.GetBuildPlatformName(platform, architecture);
    const String logFile = data.CacheDirectory / TEXT("AOTLog.txt");
    String args = String::Format(
        TEXT("-log -logfile=\"{}\" -runDotNetAOT -mutex -platform={} -arch={} -configuration={} -aotMode={} -binaries=\"{}\" -intermediate=\"{}\" {}"),
        logFile, platform, architecture, configuration, ToString(aotMode), data.DataOutputPath, data.ManagedCodeOutputPath, data.GetDotnetCommandArg());
    if (!buildSettings.SkipUnusedDotnetLibsPackaging)
        args += TEXT(" -skipUnusedDotnetLibs=false"); // Run AOT on whole class library (not just used libs)
    for (const String& define : data.CustomDefines)
    {
        args += TEXT(" -D");
        args += define;
    }
    if (ScriptsBuilder::RunBuildTool(args))
    {
        data.Error(TEXT("Failed to precompile game scripts."));
        return true;
    }

    return false;
}
