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

#include "ScreenSpaceReflectionsPass.h"
#include "ReflectionsPass.h"
#include "GBufferPass.h"
#include "RenderList.h"
#include "GlobalSignDistanceFieldPass.h"
#include "GI/GlobalSurfaceAtlasPass.h"
#include "Utils/MultiScaler.h"
#include "Engine/Engine/Engine.h"
#include "Engine/Platform/Window.h"
#include "Engine/Content/Content.h"
#include "Engine/Graphics/Graphics.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/RenderTargetPool.h"
#include "Engine/Graphics/RenderBuffers.h"
#include "Engine/Graphics/GPUContext.h"

// Shader input texture slots mapping
#define TEXTURE0 4
#define TEXTURE1 5
#define TEXTURE2 6

#define SSR_USE_HZB 1

GPU_CB_STRUCT(Data {
    ShaderGBufferData GBuffer;
    float MaxColorMiplevel;
    float TraceSizeMax;
    float MaxTraceSamples;
    float RoughnessFade;
    Float2 SSRTexelSize;
    float TemporalTime;
    float BRDFBias;
    float WorldAntiSelfOcclusionBias;
    float EdgeFadeFactor;
    float TemporalResponse;
    uint32 DepthMips;
    float RayTraceStep;
    float TemporalEffect;
    float Intensity;
    float FadeOutDistance;
    Matrix ViewMatrix;
    Matrix ViewProjectionMatrix;
    GlobalSignDistanceFieldPass::ConstantsData GlobalSDF;
    GlobalSurfaceAtlasPass::ConstantsData GlobalSurfaceAtlas;
    });

String ScreenSpaceReflectionsPass::ToString() const
{
    return TEXT("ScreenSpaceReflectionsPass");
}

bool ScreenSpaceReflectionsPass::Init()
{
    // Create pipeline states
    _psRayTracePass.CreatePipelineStates();
    _psResolvePass.CreatePipelineStates();
    _psCombinePass = GPUDevice::Instance->CreatePipelineState();
    _psTemporalPass = GPUDevice::Instance->CreatePipelineState();

    // Load assets
    _shader = Content::LoadAsyncInternal<Shader>(TEXT("Shaders/SSR"));
    _preIntegratedGF = Content::LoadAsyncInternal<Texture>(PRE_INTEGRATED_GF_ASSET_NAME);
    if (_shader == nullptr || _preIntegratedGF == nullptr)
        return true;
#if COMPILE_WITH_DEV_ENV
    _shader.Get()->OnReloading.Bind<ScreenSpaceReflectionsPass, &ScreenSpaceReflectionsPass::OnShaderReloading>(this);
#endif

    return false;
}

bool ScreenSpaceReflectionsPass::setupResources()
{
    if (!_preIntegratedGF->IsLoaded())
        return true;
    if (!_shader->IsLoaded())
        return true;
    const auto shader = _shader->GetShader();
    CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data);

    // Create pipeline stages
    GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
    if (!_psRayTracePass.IsValid())
    {
        if (_psRayTracePass.Create(psDesc, shader, "PS_RayTracePass"))
            return true;
    }
    if (!_psCombinePass->IsValid())
    {
        psDesc.PS = shader->GetPS("PS_CombinePass");
        if (_psCombinePass->Init(psDesc))
            return true;
    }
    if (!_psResolvePass.IsValid())
    {
        if (_psResolvePass.Create(psDesc, shader, "PS_ResolvePass"))
            return true;
    }
    if (!_psTemporalPass->IsValid())
    {
        psDesc.PS = shader->GetPS("PS_TemporalPass");
        if (_psTemporalPass->Init(psDesc))
            return true;
    }

    return false;
}

void ScreenSpaceReflectionsPass::Dispose()
{
    // Base
    RendererPass::Dispose();

    // Cleanup
    SAFE_DELETE_GPU_RESOURCE(_psCombinePass);
    SAFE_DELETE_GPU_RESOURCE(_psTemporalPass);
    _psRayTracePass.Delete();
    _psResolvePass.Delete();
    _shader = nullptr;
    _preIntegratedGF = nullptr;
}

GPUTexture* ScreenSpaceReflectionsPass::Render(RenderContext& renderContext, GPUTextureView* reflectionsRT, GPUTextureView* lightBuffer)
{
    // Skip pass if resources aren't ready
    if (checkIfSkipPass())
        return nullptr;
    const RenderView& view = renderContext.View;
    RenderBuffers* buffers = renderContext.Buffers;

    // TODO: add support for SSR in ortho projection
    if (view.IsOrthographicProjection())
        return nullptr;

    PROFILE_GPU_CPU("Screen Space Reflections");

    // Cache data
    auto device = GPUDevice::Instance;
    auto context = device->GetMainContext();
    const auto shader = _shader->GetShader();
    auto cb = shader->GetCB(0);
    auto& settings = renderContext.List->Settings.ScreenSpaceReflections;
    const bool useTemporal = settings.TemporalEffect && !renderContext.Task->IsCameraCut && renderContext.List->Setup.UseMotionVectors;

    // Prepare resolutions for passes
    const int32 width = buffers->GetWidth();
    const int32 height = buffers->GetHeight();
    if (width < 4 || height < 4)
        return nullptr;
    const int32 traceWidth = RenderTools::GetResolution(width, settings.RayTracePassResolution);
    const int32 traceHeight = RenderTools::GetResolution(height, settings.RayTracePassResolution);
    const int32 resolveWidth = RenderTools::GetResolution(width, settings.ResolvePassResolution);
    const int32 resolveHeight = RenderTools::GetResolution(height, settings.ResolvePassResolution);
    const int32 colorBufferWidth = RenderTools::GetResolution(width, ResolutionMode::Half);
    const int32 colorBufferHeight = RenderTools::GetResolution(height, ResolutionMode::Half);
    const auto colorBufferMips = MipLevelsCount(colorBufferWidth, colorBufferHeight);

    // Prepare buffers
    GPUTexture* colorBuffer0, *colorBuffer1;
    if (settings.UseColorBufferMips)
    {
        auto tempDesc = GPUTextureDescription::New2D(colorBufferWidth, colorBufferHeight, 0, PixelFormat::R11G11B10_Float, GPUTextureFlags::ShaderResource | GPUTextureFlags::RenderTarget | GPUTextureFlags::PerMipViews);
        colorBuffer0 = RenderTargetPool::Get(tempDesc);
        RENDER_TARGET_POOL_SET_NAME(colorBuffer0, "SSR.ColorBuffer0");
        // TODO: maybe allocate colorBuffer1 smaller because mip0 is not used (the same as PostProcessingPass for Bloom), keep in sync to use the same buffer in frame
        colorBuffer1 = RenderTargetPool::Get(tempDesc);
        RENDER_TARGET_POOL_SET_NAME(colorBuffer1, "SSR.ColorBuffer1");
    }
    else
    {
        // Single mip
        auto tempDesc = GPUTextureDescription::New2D(colorBufferWidth, colorBufferHeight, 1, PixelFormat::R11G11B10_Float);
        colorBuffer0 = RenderTargetPool::Get(tempDesc);
        colorBuffer1 = nullptr;
    }
    GPUTexture* traceBuffer, *resolveBuffer;
    {
        auto tempDesc = GPUTextureDescription::New2D(traceWidth, traceHeight, PixelFormat::R16G16B16A16_Float);
        traceBuffer = RenderTargetPool::Get(tempDesc);
        RENDER_TARGET_POOL_SET_NAME(traceBuffer, "SSR.TraceBuffer");
        tempDesc.Width = resolveWidth;
        tempDesc.Height = resolveHeight;
        resolveBuffer = RenderTargetPool::Get(tempDesc);
        RENDER_TARGET_POOL_SET_NAME(resolveBuffer, "SSR.ResolveBuffer");
    }

    // Pick effect settings
    int32 maxTraceSamples = 60;
    int32 resolveSamples = settings.ResolveSamples;
    switch (Graphics::SSRQuality)
    {
    case Quality::Low:
        maxTraceSamples = 40;
        resolveSamples = Math::Min(resolveSamples, 2);
        break;
    case Quality::Medium:
        maxTraceSamples = 55;
        resolveSamples = Math::Min(resolveSamples, 4);
        break;
    case Quality::High:
        maxTraceSamples = 70;
        break;
    case Quality::Ultra:
        maxTraceSamples = 120;
        break;
    }
    int32 resolvePassIndex = 0;
    if (resolveSamples >= 8)
        resolvePassIndex = 3;
    else if (resolveSamples >= 4)
        resolvePassIndex = 2;
    else if (resolveSamples >= 2)
        resolvePassIndex = 1;

    // Setup data
    Data data;
    GBufferPass::SetInputs(view, data.GBuffer);
    data.RoughnessFade = Math::Saturate(settings.RoughnessThreshold);
    data.MaxTraceSamples = static_cast<float>(maxTraceSamples);
    data.BRDFBias = settings.BRDFBias;
    data.WorldAntiSelfOcclusionBias = settings.WorldAntiSelfOcclusionBias * (int32)settings.DepthResolution;
    data.EdgeFadeFactor = settings.EdgeFadeFactor;
    data.SSRTexelSize = Float2(1.0f / (float)traceWidth, 1.0f / (float)traceHeight);
    data.TraceSizeMax = (float)Math::Max(traceWidth, traceHeight);
    data.MaxColorMiplevel = settings.UseColorBufferMips ? (float)(colorBufferMips - 2) : 0.0f;
    data.RayTraceStep = (float)settings.DepthResolution / (float)width;
    data.Intensity = settings.Intensity;
    data.FadeOutDistance = Math::Max(settings.FadeOutDistance, 100.0f);
    data.TemporalResponse = settings.TemporalResponse;
    data.TemporalEffect = useTemporal ? 1.0f : 0.0f;
    data.TemporalTime = useTemporal ? RenderTools::ComputeTemporalTime() : 0;
    Matrix::Transpose(view.View, data.ViewMatrix);
    Matrix::Transpose(view.ViewProjection(), data.ViewProjectionMatrix);

    // Try to use Global Surface Atlas (with rendered GI) to perform full-scene tracing (not only screen-space)
    bool useGlobalSurfaceAtlas = false;
    GlobalSignDistanceFieldPass::BindingData bindingDataSDF;
    GlobalSurfaceAtlasPass::BindingData bindingDataSurfaceAtlas;
    if (settings.TraceMode == ReflectionsTraceMode::SoftwareTracing &&
        EnumHasAnyFlags(view.Flags, ViewFlags::GI) &&
        renderContext.List->Settings.GlobalIllumination.Mode == GlobalIlluminationMode::DDGI)
    {
        if (!GlobalSignDistanceFieldPass::Instance()->Render(renderContext, context, bindingDataSDF) &&
            !GlobalSurfaceAtlasPass::Instance()->Render(renderContext, context, bindingDataSurfaceAtlas))
        {
            useGlobalSurfaceAtlas = true;
            data.GlobalSDF = bindingDataSDF.Constants;
            data.GlobalSurfaceAtlas = bindingDataSurfaceAtlas.Constants;
        }
    }

    // Prepare depth buffer
#if SSR_USE_HZB
    int32 hzbMips = settings.DepthResolution == ResolutionMode::Full ? 5 : 4; // Using lower mips in tracing introduces blocky artifacts
    bool hzbFullRes = settings.DepthResolution == ResolutionMode::Full;
    GPUTexture* depthBufferTrace = buffers->RequestHiZ(context, hzbFullRes, hzbMips);
    data.DepthMips = hzbMips - 1; // Offset to improve SSR range
#else
    GPUTexture* depthBufferTrace = settings.DepthResolution == ResolutionMode::Half ? buffers->RequestHalfResDepth(context) : buffers->DepthBuffer;
    data.DepthMips = 1;
#endif
    if (depthBufferTrace == nullptr)
    {
        RenderTargetPool::Release(colorBuffer0);
        RenderTargetPool::Release(colorBuffer1);
        RenderTargetPool::Release(traceBuffer);
        RenderTargetPool::Release(resolveBuffer);
        return nullptr;
    }

    // Prepare constants
    context->UpdateCB(cb, &data);
    context->BindCB(0, cb);

    // Combine pass
    {
        PROFILE_GPU("Combine");
        context->BindSR(0, buffers->GBuffer0);
        context->BindSR(1, buffers->GBuffer1);
        context->BindSR(2, buffers->GBuffer2);
        context->BindSR(3, buffers->DepthBuffer);
        context->BindSR(TEXTURE0, lightBuffer);
        context->BindSR(TEXTURE1, reflectionsRT);
        context->BindSR(TEXTURE2, _preIntegratedGF->GetTexture());
        context->SetViewportAndScissors((float)colorBufferWidth, (float)colorBufferHeight);
        context->SetRenderTarget(colorBuffer0->View(0));
        context->SetState(_psCombinePass);
        context->DrawFullscreenTriangle();
        context->UnBindSR(TEXTURE1);
        context->UnBindSR(TEXTURE2);
        context->ResetRenderTarget();
    }

    // Blur Pass
    if (settings.UseColorBufferMips)
    {
        // Note: using color buffer mips maps helps with reducing artifacts
        // and improves resolve pass performance (faster color texture lookups, less cache misses)
        // Also for high surface roughness values it adds more blur to the reflection tail which looks more realistic.

        // Downscale with gaussian blur
        auto filterMode = PLATFORM_WEB || PLATFORM_ANDROID || PLATFORM_IOS || PLATFORM_SWITCH ? MultiScaler::FilterMode::GaussianBlur5 : MultiScaler::FilterMode::GaussianBlur9;
        for (int32 mipLevel = 1; mipLevel < colorBufferMips; mipLevel++)
        {
            const int32 mipWidth = Math::Max(colorBufferWidth >> mipLevel, 1);
            const int32 mipHeight = Math::Max(colorBufferHeight >> mipLevel, 1);

            const auto srcMip = colorBuffer0->View(0, mipLevel - 1);
            const auto tmpMip = colorBuffer1->View(0, mipLevel);
            const auto dstMip = colorBuffer0->View(0, mipLevel);

            MultiScaler::Instance()->Filter(filterMode, context, mipWidth, mipHeight, srcMip, dstMip, tmpMip);
        }

        // Restore state
        context->BindCB(0, cb);
        context->BindSR(0, buffers->GBuffer0);
    }
    RenderTargetPool::Release(colorBuffer1);

    // Ray Trace Pass
    {
        PROFILE_GPU("RayTrace");
        context->SetViewportAndScissors((float)traceWidth, (float)traceHeight);
        context->SetRenderTarget(*traceBuffer);
        context->BindSR(3, depthBufferTrace);
        context->BindSR(TEXTURE0, colorBuffer0->View());
        if (useGlobalSurfaceAtlas)
        {
            context->BindSR(7, bindingDataSDF.Texture ? bindingDataSDF.Texture->ViewVolume() : nullptr);
            context->BindSR(8, bindingDataSDF.TextureMip ? bindingDataSDF.TextureMip->ViewVolume() : nullptr);
            context->BindSR(9, bindingDataSurfaceAtlas.Chunks ? bindingDataSurfaceAtlas.Chunks->View() : nullptr);
            context->BindSR(10, bindingDataSurfaceAtlas.CulledObjects ? bindingDataSurfaceAtlas.CulledObjects->View() : nullptr);
            context->BindSR(11, bindingDataSurfaceAtlas.Objects ? bindingDataSurfaceAtlas.Objects->View() : nullptr);
            context->BindSR(12, bindingDataSurfaceAtlas.AtlasDepth->View());
            context->BindSR(13, bindingDataSurfaceAtlas.AtlasLighting->View());
        }
        context->SetState(_psRayTracePass.Get(useGlobalSurfaceAtlas ? 1 : 0));
        context->DrawFullscreenTriangle();
        context->ResetRenderTarget();
        RenderTargetPool::Release(colorBuffer0);
    }

    // Resolve Pass
    {
        PROFILE_GPU("Resolve");
        context->SetViewportAndScissors((float)resolveWidth, (float)resolveHeight);
        context->SetRenderTarget(resolveBuffer->View());
        context->BindSR(TEXTURE0, traceBuffer->View());
        context->SetState(_psResolvePass.Get(resolvePassIndex));
        context->DrawFullscreenTriangle();
        context->ResetRenderTarget();
        RenderTargetPool::Release(traceBuffer);
    }

    // Temporal Pass
    GPUTexture* reflectionsBuffer = resolveBuffer;
    if (useTemporal)
    {
        PROFILE_GPU("Temporal");
        buffers->LastFrameTemporalSSR = Engine::FrameCount;
        bool resetHistory = false;
        if (!buffers->TemporalSSR || buffers->TemporalSSR->Width() != resolveWidth || buffers->TemporalSSR->Height() != resolveHeight)
        {
            resetHistory = true;
            if (buffers->TemporalSSR)
                RenderTargetPool::Release(buffers->TemporalSSR);
            auto tempDesc = GPUTextureDescription::New2D(resolveWidth, resolveHeight, PixelFormat::R16G16B16A16_Float);
            buffers->TemporalSSR = RenderTargetPool::Get(tempDesc);
            RENDER_TARGET_POOL_SET_NAME(buffers->TemporalSSR, "SSR.TemporalSSR");
        }
        auto newTemporal = RenderTargetPool::Get(buffers->TemporalSSR->GetDescription());
        RENDER_TARGET_POOL_SET_NAME(newTemporal, "SSR.TemporalSSR");

        if (resetHistory)
        {
            context->Draw(newTemporal, resolveBuffer);
        }
        else
        {
            context->SetRenderTarget(newTemporal->View());
            context->BindSR(TEXTURE0, resolveBuffer);
            context->BindSR(TEXTURE1, buffers->TemporalSSR);
            context->BindSR(TEXTURE2, buffers->MotionVectors && buffers->MotionVectors->IsAllocated() ? buffers->MotionVectors->View() : nullptr);
            context->SetState(_psTemporalPass);
            context->DrawFullscreenTriangle();
        }
        context->ResetRenderTarget();
        context->UnBindSR(TEXTURE1);
        context->UnBindSR(TEXTURE2);

        RenderTargetPool::Release(resolveBuffer);
        context->CopyResource(buffers->TemporalSSR, newTemporal);
        reflectionsBuffer = newTemporal;
    }

    return reflectionsBuffer;
}

#if COMPILE_WITH_DEV_ENV

void ScreenSpaceReflectionsPass::OnShaderReloading(Asset* obj)
{
    _psCombinePass->ReleaseGPU();
    _psTemporalPass->ReleaseGPU();
    _psRayTracePass.Release();
    _psResolvePass.Release();
    invalidateResources();
}

#endif
