// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

#include "Builder.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Level/Scene/Scene.h"
#include "Engine/Level/Level.h"
#include "Engine/Content/Content.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Types/TimeSpan.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Engine/Globals.h"
#include "Engine/Threading/ThreadSpawner.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/GPUBuffer.h"
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Graphics/GPUPipelineState.h"
#include "Engine/Graphics/RenderTargetPool.h"
#include "Engine/Graphics/Shaders/GPUShader.h"

namespace ShadowsOfMordor
{
    bool IsRunningRadiancePass = false;
    bool EnableLightmapsUsage = true;

    float BuildProgressStepProgress[9] =
    {
        0.01f,
        // Initialize
        0.017f,
        // CacheEntries,
        0.002f,
        // GenerateLightmapCharts,
        0.002f,
        // PackLightmapCharts,
        0.028f,
        // UpdateLightmapsCollection,
        0.004f,
        // UpdateEntries,
        0.018f,
        // GenerateHemispheresCache,
        0.90f,
        // RenderHemispheres,
        0.01f,
        // Cleanup,
    }; // Sum == 1

    PixelFormat HemispheresFormatToPixelFormat[2] =
    {
        PixelFormat::R32G32B32A32_Float,
        PixelFormat::R16G16B16A16_Float,
    };

    float GetProgressBeforeStep(BuildProgressStep step)
    {
        float sum = 0;
        for (int32 i = 0; i < static_cast<int32>(step); i++)
            sum += BuildProgressStepProgress[i];
        return sum;
    }

    float GetProgressWithStep(BuildProgressStep step)
    {
        float sum = 0;
        for (int32 i = 0; i <= static_cast<int32>(step); i++)
            sum += BuildProgressStepProgress[i];
        return sum;
    }
}

class ShadowsOfMordorBuilderService : public EngineService
{
public:

    ShadowsOfMordorBuilderService()
        : EngineService(TEXT("ShadowsOfMordor Builder"), 80)
    {
    }

    void Dispose() override;
};

ShadowsOfMordorBuilderService ShadowsOfMordorBuilderServiceInstance;

ShadowsOfMordor::Builder::Builder()
    : _wasBuildCalled(false)
    , _isActive(false)
    , _wasBuildCancelled(false)
{
}

void ShadowsOfMordor::Builder::Build()
{
    // To bake static lighting we have to support compute shaders
    ASSERT_LOW_LAYER(GPUDevice::Instance && GPUDevice::Instance->Limits.HasCompute);

    _locker.Lock();

    if (!_wasBuildCalled)
    {
        _wasBuildCalled = true;
        _wasBuildCancelled = 0;

        // Ensure any scene has been loaded
        ASSERT(Level::IsAnySceneLoaded());

        // Register background work
        Function<int32()> f;
        f.Bind<Builder, &Builder::doWork>(this);
        ThreadSpawner::Start(f, TEXT("GI Baking"));
    }

    _locker.Unlock();
}

void ShadowsOfMordor::Builder::CancelBuild()
{
    _locker.Lock();

    if (_wasBuildCalled)
    {
        Platform::AtomicStore(&_wasBuildCancelled, 1);
    }

    _locker.Unlock();
}

void ShadowsOfMordor::Builder::Dispose()
{
    _locker.Lock();
    const bool waitForEnd = _wasBuildCalled;
    CancelBuild();
    _locker.Unlock();

    if (waitForEnd)
    {
        // Lightmaps builder must respond always withing 100ms after cancel work signal!
        Platform::Sleep(100);
    }

    releaseResources();
}

#if HEMISPHERES_BAKE_STATE_SAVE

#include "Engine/Platform/FileSystem.h"
#include "Engine/Platform/MessageBox.h"
#include "Engine/Serialization/FileReadStream.h"
#include "Engine/Serialization/FileWriteStream.h"
#include "Engine/Engine/CommandLine.h"
#include "Engine/Scripting/Scripting.h"
#include "FlaxEngine.Gen.h"

namespace ShadowsOfMordor
{
    const Char* StateCacheFileName = TEXT("ShadowsOfMordor_Cache.bin");
}

#endif

void ShadowsOfMordor::Builder::CheckIfRestoreState()
{
#if HEMISPHERES_BAKE_STATE_SAVE
    // Check if there is a state to restore
    const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
    if (!FileSystem::FileExists(path))
        return;

    // Ask user if restore state
    if (!CommandLine::Options.Headless.IsTrue() && MessageBox::Show(TEXT("The last Lightmaps Baking job had crashed. Do you want to restore the state and continue baking?"), TEXT("Restore lightmaps baking?"), MessageBoxButtons::YesNo, MessageBoxIcon::Question) != DialogResult::Yes)
    {
        deleteState();
        return;
    }

    // Skip compilation on startup so editor will just load binaries
    CommandLine::Options.SkipCompile = true;
#endif
}

bool ShadowsOfMordor::Builder::RestoreState()
{
#if HEMISPHERES_BAKE_STATE_SAVE
    // Check if there is a state to restore
    const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
    if (!FileSystem::FileExists(path))
        return false;

    // Open file
    LOG(Info, "Restoring the lightmaps baking state...");
    auto stream = FileReadStream::Open(path);
    int32 version;
    stream->ReadInt32(&version);
    if (version != 1)
    {
        LOG(Error, "Invalid version.");
        Delete(stream);
        deleteState();
        return false;
    }
    int32 scenesCount;
    stream->ReadInt32(&scenesCount);

    // Open scenes used during baking
    for (int32 i = 0; i < scenesCount; i++)
    {
        Guid id;
        stream->Read(id);
        Level::LoadScene(id);
    }

    Delete(stream);

    return true;
#else
    return false;
#endif
}

void ShadowsOfMordor::Builder::saveState()
{
#if HEMISPHERES_BAKE_STATE_SAVE
    const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
    const auto pathTmp = path + TEXT(".tmp");
    auto stream = FileWriteStream::Open(pathTmp);

    LOG(Info, "Saving the lightmaps baking state (scene: {0}, lightmap: {1}, hemisphere: {2})", _workerActiveSceneIndex, _workerStagePosition0, _workerStagePosition1);

    // Save all scenes on first state saving (actors have modified lightmap entries mapping to the textures and scene lightmaps list has been edited)
    if (_firstStateSave)
    {
        _firstStateSave = false;
        Level::SaveAllScenes();
    }

    // Format version
    stream->WriteInt32(1);

    // Scenes ids
    stream->WriteInt32(_scenes.Count());
    for (int32 i = 0; i < _scenes.Count(); i++)
        stream->Write(_scenes[i]->Scene->GetID());

    // State
    stream->WriteInt32(_giBounceRunningIndex);
    stream->WriteInt32(_bounceCount);
    stream->WriteInt32(_workerActiveSceneIndex);
    stream->WriteInt32(_workerStagePosition0);
    stream->WriteInt32(_workerStagePosition1);
    stream->WriteFloat(_hemisphereTexelsTotalWeight);

    // Scenes data
    for (int32 sceneIndex = 0; sceneIndex < _scenes.Count(); sceneIndex++)
    {
        auto& scene = _scenes[sceneIndex];
        stream->WriteInt32(scene->LightmapsCount);
        stream->WriteInt32(scene->HemispheresCount);
        stream->WriteInt32(scene->MergedHemispheresCount);

        if (scene->LightmapsCount == 0)
            continue;
        auto lightmapDataStaging = scene->Lightmaps[0].LightmapData->ToStagingReadback();
        for (int32 lightmapIndex = 0; lightmapIndex < scene->LightmapsCount; lightmapIndex++)
        {
            auto& lightmap = scene->Lightmaps[lightmapIndex];

            // Hemispheres
            stream->WriteInt32(lightmap.Hemispheres.Count());
            stream->WriteBytes(lightmap.Hemispheres.Get(), lightmap.Hemispheres.Count() * sizeof(HemisphereData));

            // Lightmap Data
            // TODO: instead of doing hackish flush/sleep just copy data to some temporary buffer one frame before saving the state
            ASSERT(GPUDevice::Instance->IsRendering());
            auto context = GPUDevice::Instance->GetMainContext();
            const int32 lightmapDataSize = lightmapDataStaging->GetSize();
            context->CopyBuffer(lightmapDataStaging, lightmap.LightmapData, lightmapDataSize);
            context->Flush();
            Platform::Sleep(10);
            void* mapped = lightmapDataStaging->Map(GPUResourceMapMode::Read);
            ASSERT(mapped);
            stream->WriteInt32(lightmapDataSize);
            stream->WriteBytes(mapped, lightmapDataSize);
            lightmapDataStaging->Unmap();
        }
        SAFE_DELETE_GPU_RESOURCE(lightmapDataStaging);
    }

    Delete(stream);

    // Update the cache file
    if (FileSystem::FileExists(path))
        FileSystem::DeleteFile(path);
    FileSystem::MoveFile(path, pathTmp);
    FileSystem::DeleteFile(pathTmp);

    _lastStateSaveTime = DateTime::Now();
#endif
}

bool ShadowsOfMordor::Builder::loadState()
{
#if HEMISPHERES_BAKE_STATE_SAVE
    const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
    if (!FileSystem::FileExists(path))
        return false;

    // Open file
    LOG(Info, "Loading the lightmaps baking state...");
    auto stream = FileReadStream::Open(path);
    int32 version;
    stream->ReadInt32(&version);
    if (version != 1)
    {
        LOG(Error, "Invalid version.");
        Delete(stream);
        deleteState();
        return false;
    }
    int32 scenesCount;
    stream->ReadInt32(&scenesCount);

    // Verify if scenes used during baking are loaded
    if (Level::Scenes.Count() != scenesCount || scenesCount != _scenes.Count())
    {
        LOG(Error, "Invalid scenes.");
        Delete(stream);
        deleteState();
        return false;
    }
    for (int32 i = 0; i < scenesCount; i++)
    {
        Guid id;
        stream->Read(id);
        if (Level::Scenes[i]->GetID() != id || _scenes[i]->SceneIndex != i)
        {
            LOG(Error, "Invalid scenes.");
            Delete(stream);
            deleteState();
            return false;
        }
    }

    // State
    stream->ReadInt32(&_giBounceRunningIndex);
    stream->ReadInt32(&_bounceCount);
    stream->ReadInt32(&_workerActiveSceneIndex);
    stream->ReadInt32(&_workerStagePosition0);
    stream->ReadInt32(&_workerStagePosition1);
    stream->ReadFloat(&_hemisphereTexelsTotalWeight);

    // Scenes data
    for (int32 sceneIndex = 0; sceneIndex < _scenes.Count(); sceneIndex++)
    {
        auto& scene = _scenes[sceneIndex];
        stream->ReadInt32(&scene->LightmapsCount);
        stream->ReadInt32(&scene->HemispheresCount);
        stream->ReadInt32(&scene->MergedHemispheresCount);

        scene->Lightmaps.Resize(scene->LightmapsCount);
        if (scene->LightmapsCount == 0)
            continue;
        for (int32 lightmapIndex = 0; lightmapIndex < scene->LightmapsCount; lightmapIndex++)
        {
            auto& lightmap = scene->Lightmaps[lightmapIndex];
            lightmap.Init(&scene->GetSettings());

            // Hemispheres
            int32 hemispheresCount;
            stream->ReadInt32(&hemispheresCount);
            lightmap.Hemispheres.Resize(hemispheresCount);
            stream->ReadBytes(lightmap.Hemispheres.Get(), lightmap.Hemispheres.Count() * sizeof(HemisphereData));

            // Lightmap Data
            int32 lightmapDataSize;
            stream->ReadInt32(&lightmapDataSize);
            const auto lightmapData = lightmap.LightmapData;
            if (lightmapDataSize != lightmapData->GetSize())
            {
                LOG(Error, "Invalid lightmap data size.");
                Delete(stream);
                deleteState();
                return false;
            }
            lightmap.LightmapDataInit.Resize(lightmapDataSize);
            stream->ReadBytes(lightmap.LightmapDataInit.Get(), lightmapDataSize);
        }
    }

    Delete(stream);

    _firstStateSave = false;
    _lastStateSaveTime = DateTime::Now();
    return true;
#else
    return false;
#endif
}

void ShadowsOfMordor::Builder::deleteState()
{
#if HEMISPHERES_BAKE_STATE_SAVE
    const auto path = Globals::ProjectCacheFolder / StateCacheFileName;
    if (FileSystem::FileExists(path))
        FileSystem::DeleteFile(path);
#endif
}

void ShadowsOfMordor::Builder::reportProgress(BuildProgressStep step, float stepProgress)
{
    const auto currentStepTotalProgress = BuildProgressStepProgress[(int32)step];

    // Apply scenes progress
    //stepProgress = (_workerActiveSceneIndex + stepProgress) / _scenes.Count();

    // Get progress in 'before' steps
    reportProgress(step, stepProgress, GetProgressBeforeStep(step) + stepProgress * currentStepTotalProgress);
}

void ShadowsOfMordor::Builder::reportProgress(BuildProgressStep step, float stepProgress, float totalProgress)
{
    // Send step changes info
    if (_lastStep != step)
    {
        const auto now = DateTime::Now();
        LOG(Info, "Lightmaps baking step {0} time: {1}s", ShadowsOfMordor::ToString(_lastStep), Math::RoundToInt((float)(now - _lastStepStart).GetTotalSeconds()));

        _lastStep = step;
        _lastStepStart = now;
    }

    // Send event
    OnBuildProgress(step, stepProgress, totalProgress);
}

bool ShadowsOfMordor::Builder::initResources()
{
    // TODO: remove this release and just create missing resources
    releaseResources();

    _output = GPUDevice::Instance->CreateTexture(TEXT("Output"));
    if (_output->Init(GPUTextureDescription::New2D(HEMISPHERES_RESOLUTION, HEMISPHERES_RESOLUTION, PixelFormat::R11G11B10_Float)))
        return true;
    _task = New<BuilderRenderTask>();
    _task->Enabled = false;
    _task->Output = _output;
    auto& view = _task->View;
    view.Mode = ViewMode::NoPostFx;
    view.Flags =
            ViewFlags::GI |
            ViewFlags::DirectionalLights |
            ViewFlags::PointLights |
            ViewFlags::SpotLights |
            ViewFlags::Sky |
            ViewFlags::Shadows |
            ViewFlags::Decals |
            ViewFlags::SkyLights |
            ViewFlags::Reflections;
    view.IsOfflinePass = true;
    view.Near = HEMISPHERES_NEAR_PLANE;
    view.Far = HEMISPHERES_FAR_PLANE;
    view.StaticFlagsMask = view.StaticFlagsCompare = StaticFlags::Lightmap;
    view.MaxShadowsQuality = Quality::Low;
    _task->Resize(HEMISPHERES_RESOLUTION, HEMISPHERES_RESOLUTION);

    // Load shader
    _shader = Content::LoadAsyncInternal<Shader>(TEXT("Shaders/BakeLightmap"));
    if (_shader == nullptr)
        return true;
    if (_shader->WaitForLoaded())
        return true;

    _psRenderCacheModel = GPUDevice::Instance->CreatePipelineState();
    GPUPipelineState::Description desc = GPUPipelineState::Description::DefaultNoDepth;
    desc.CullMode = CullMode::TwoSided;
    desc.VS = _shader->GetShader()->GetVS("VS_RenderCacheModel");
    desc.PS = _shader->GetShader()->GetPS("PS_RenderCache");
    if (_psRenderCacheModel->Init(desc))
        return true;

    _psRenderCacheTerrain = GPUDevice::Instance->CreatePipelineState();
    desc.VS = _shader->GetShader()->GetVS("VS_RenderCacheTerrain");
    if (_psRenderCacheTerrain->Init(desc))
        return true;

    _psBlurCache = GPUDevice::Instance->CreatePipelineState();
    desc = GPUPipelineState::Description::DefaultFullscreenTriangle;
    desc.PS = _shader->GetShader()->GetPS("PS_BlurCache");
    if (_psBlurCache->Init(desc))
        return true;

    _irradianceReduction = GPUDevice::Instance->CreateBuffer(TEXT("IrradianceReduction"));
    if (_irradianceReduction->Init(GPUBufferDescription::Typed(HEMISPHERES_RESOLUTION * NUM_SH_TARGETS, HemispheresFormatToPixelFormat[HEMISPHERES_IRRADIANCE_FORMAT], true)))
        return true;

#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
    releaseDebugHemisphereAtlases();
#endif

    return false;
}

void ShadowsOfMordor::Builder::releaseResources()
{
#if DEBUG_EXPORT_HEMISPHERES_PREVIEW
    releaseDebugHemisphereAtlases();
#endif

    SAFE_DELETE_GPU_RESOURCE(_psRenderCacheModel);
    SAFE_DELETE_GPU_RESOURCE(_psRenderCacheTerrain);
    SAFE_DELETE_GPU_RESOURCE(_psBlurCache);
    _shader = nullptr;

    SAFE_DELETE_GPU_RESOURCE(_irradianceReduction);

    RenderTargetPool::Release(_cachePositions);
    _cachePositions = nullptr;
    RenderTargetPool::Release(_cacheNormals);
    _cacheNormals = nullptr;

    if (_output)
        _output->ReleaseGPU();

    SAFE_DELETE(_task);
    SAFE_DELETE_GPU_RESOURCE(_output);
}

bool ShadowsOfMordor::Builder::waitForJobDataSync()
{
    bool wasCancelled = false;
    const int32 framesToSyncCount = 4;

    while (!wasCancelled)
    {
        Platform::Sleep(1);

        wasCancelled = checkBuildCancelled();
        if (_lastJobFrame + framesToSyncCount <= Engine::FrameCount)
            break;
    }

    return wasCancelled;
}

void ShadowsOfMordorBuilderService::Dispose()
{
    ShadowsOfMordor::Builder::Instance()->Dispose();
}
