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

#include "MultiScaler.h"
#include "Engine/Graphics/Textures/GPUTexture.h"
#include "Engine/Graphics/GPUContext.h"
#include "Engine/Content/Content.h"

GPU_CB_STRUCT(Data {
    Float2 TexelSize;
    Float2 Padding;
    });

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

bool MultiScaler::Init()
{
    // Create pipeline states
    _psBlur5.CreatePipelineStates();
    _psBlur9.CreatePipelineStates();
    _psBlur13.CreatePipelineStates();
    _psHalfDepth.CreatePipelineStates();
    _psUpscale = GPUDevice::Instance->CreatePipelineState();

    // Load asset
    _shader = Content::LoadAsyncInternal<Shader>(TEXT("Shaders/MultiScaler"));
    if (_shader == nullptr)
        return true;
#if COMPILE_WITH_DEV_ENV
    _shader.Get()->OnReloading.Bind<MultiScaler, &MultiScaler::OnShaderReloading>(this);
#endif

    return false;
}

bool MultiScaler::setupResources()
{
    // Check if shader has not been loaded
    if (!_shader->IsLoaded())
        return true;
    const auto shader = _shader->GetShader();
    CHECK_INVALID_SHADER_PASS_CB_SIZE(shader, 0, Data);

    // Create pipeline states
    GPUPipelineState::Description psDesc = GPUPipelineState::Description::DefaultFullscreenTriangle;
    if (!_psBlur5.IsValid())
    {
        if (_psBlur5.Create(psDesc, shader, "PS_Blur5"))
            return true;
    }
    if (!_psBlur9.IsValid())
    {
        if (_psBlur9.Create(psDesc, shader, "PS_Blur9"))
            return true;
    }
    if (!_psBlur13.IsValid())
    {
        if (_psBlur13.Create(psDesc, shader, "PS_Blur13"))
            return true;
    }
    if (!_psUpscale->IsValid())
    {
        psDesc.PS = shader->GetPS("PS_Upscale");
        if (_psUpscale->Init(psDesc))
            return true;
    }
    if (!_psHalfDepth.IsValid())
    {
        psDesc.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::Red;
        psDesc.PS = shader->GetPS("PS_HalfDepth", 0);
        if (_psHalfDepth[0]->Init(psDesc))
            return true;
        psDesc.PS = shader->GetPS("PS_HalfDepth", 2);
        if (_psHalfDepth[2]->Init(psDesc))
            return true;
        psDesc.BlendMode.RenderTargetWriteMask = BlendingMode::ColorWrite::None;
        psDesc.PS = shader->GetPS("PS_HalfDepth", 1);
        psDesc.DepthWriteEnable = true;
        psDesc.DepthEnable = true;
        psDesc.DepthFunc = ComparisonFunc::Always;
        if (_psHalfDepth[1]->Init(psDesc))
            return true;
    }

    return false;
}

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

    // Cleanup
    SAFE_DELETE_GPU_RESOURCE(_psUpscale);
    _psBlur5.Delete();
    _psBlur9.Delete();
    _psBlur13.Delete();
    _psHalfDepth.Delete();
    _shader = nullptr;
}

void MultiScaler::Filter(FilterMode mode, GPUContext* context, int32 width, int32 height, GPUTextureView* src, GPUTextureView* dst, GPUTextureView* tmp)
{
    PROFILE_GPU_CPU("MultiScaler Filter");

    context->SetViewportAndScissors((float)width, (float)height);

    // Check if has missing resources
    if (checkIfSkipPass())
    {
        // Simple copy
        context->SetRenderTarget(dst);
        context->Draw(src);
        context->ResetRenderTarget();
        return;
    }

    // Select filter
    GPUPipelineStatePermutationsPs<2>* ps;
    switch (mode)
    {
    case FilterMode::GaussianBlur5:
        ps = &_psBlur5;
        break;
    case FilterMode::GaussianBlur9:
        ps = &_psBlur9;
        break;
    case FilterMode::GaussianBlur13:
        ps = &_psBlur13;
        break;
    default:
        CRASH;
        return;
    }

    // Prepare
    Data data;
    data.TexelSize.X = 1.0f / (float)width;
    data.TexelSize.Y = 1.0f / (float)height;
    auto cb = _shader->GetShader()->GetCB(0);
    context->UpdateCB(cb, &data);
    context->BindCB(0, cb);

    // Convolve horizontal
    context->BindSR(0, src);
    context->SetRenderTarget(tmp);
    context->SetState(ps->Get(0));
    context->DrawFullscreenTriangle();

    context->ResetRenderTarget();

    // Convolve vertical
    context->BindSR(0, tmp);
    context->SetRenderTarget(dst);
    context->SetState(ps->Get(1));
    context->DrawFullscreenTriangle();

    context->ResetRenderTarget();
}

void MultiScaler::Filter(FilterMode mode, GPUContext* context, int32 width, int32 height, GPUTextureView* srcDst, GPUTextureView* tmp)
{
    PROFILE_GPU_CPU("MultiScaler Filter");

    context->SetViewportAndScissors((float)width, (float)height);

    if (checkIfSkipPass())
        return;

    // Select filter
    GPUPipelineStatePermutationsPs<2>* ps;
    switch (mode)
    {
    case FilterMode::GaussianBlur5:
        ps = &_psBlur5;
        break;
    case FilterMode::GaussianBlur9:
        ps = &_psBlur9;
        break;
    case FilterMode::GaussianBlur13:
        ps = &_psBlur13;
        break;
    default:
        CRASH;
        return;
    }

    // Prepare
    Data data;
    data.TexelSize.X = 1.0f / (float)width;
    data.TexelSize.Y = 1.0f / (float)height;
    auto cb = _shader->GetShader()->GetCB(0);
    context->UpdateCB(cb, &data);
    context->BindCB(0, cb);

    // Convolve horizontal
    context->BindSR(0, srcDst);
    context->SetRenderTarget(tmp);
    context->SetState(ps->Get(0));
    context->DrawFullscreenTriangle();

    context->ResetRenderTarget();

    // Convolve vertical
    context->BindSR(0, tmp);
    context->SetRenderTarget(srcDst);
    context->SetState(ps->Get(1));
    context->DrawFullscreenTriangle();

    context->ResetRenderTarget();
}

void MultiScaler::DownscaleDepth(GPUContext* context, int32 dstWidth, int32 dstHeight, GPUTexture* src, GPUTextureView* dst)
{
    PROFILE_GPU_CPU("Downscale Depth");
    bool outputDepth = ((GPUTexture*)dst->GetParent())->IsDepthStencil();
    if (checkIfSkipPass())
    {
        if (outputDepth)
            context->ClearDepth(dst);
        else
            context->Clear(dst, Color::Transparent);
        return;
    }

    // Prepare
    Data data;
    data.TexelSize.X = 1.0f / (float)src->Width();
    data.TexelSize.Y = 1.0f / (float)src->Height();
    auto cb = _shader->GetShader()->GetCB(0);
    context->UpdateCB(cb, &data);
    context->BindCB(0, cb);

    // Draw
    context->SetViewportAndScissors((float)dstWidth, (float)dstHeight);
    if (outputDepth)
        context->SetRenderTarget(dst, (GPUTextureView*)nullptr);
    else
        context->SetRenderTarget(dst);
    context->BindSR(0, src);
    context->SetState(_psHalfDepth[outputDepth ? 1 : 0]);
    context->DrawFullscreenTriangle();

    // Cleanup
    context->ResetRenderTarget();
    context->UnBindCB(0);
}

void MultiScaler::BuildHiZ(GPUContext* context, GPUTexture* srcDepth, GPUTexture* dstHiZ)
{
    PROFILE_GPU_CPU("Build HiZ");

    int32 dstWidth = dstHiZ->Width();
    int32 dstHeight = dstHiZ->Height();

    // Copy mip0
    if (srcDepth->Size() == dstHiZ->Size() && srcDepth->Format() == dstHiZ->Format())
    {
        context->CopySubresource(dstHiZ, 0, srcDepth, 0);
    }
    else if (srcDepth->Size() == dstHiZ->Size())
    {
        context->Draw(dstHiZ, srcDepth);
    }
    else
    {
        context->SetViewportAndScissors((float)dstWidth, (float)dstHeight);
        context->SetRenderTarget(dstHiZ->View());
        context->BindSR(0, srcDepth);
        context->SetState(_psHalfDepth[2]);
        context->DrawFullscreenTriangle();
    }

    // Build mip chain
    for (int32 mip = 1; mip < dstHiZ->MipLevels(); mip++)
    {
        const int32 mipWidth = Math::Max(dstWidth >> mip, 1);
        const int32 mipHeight = Math::Max(dstHeight >> mip, 1);
        context->ResetRenderTarget();

        context->SetViewportAndScissors((float)mipWidth, (float)mipHeight);
        context->SetRenderTarget(dstHiZ->View(0, mip));
        context->BindSR(0, dstHiZ->View(0, mip - 1));
        context->SetState(_psHalfDepth[2]);
        context->DrawFullscreenTriangle();
    }

    context->ResetRenderTarget();
    context->UnBindCB(0);
}

void MultiScaler::Upscale(GPUContext* context, const Viewport& viewport, GPUTexture* src, GPUTextureView* dst)
{
    PROFILE_GPU_CPU("Upscale");

    context->SetViewportAndScissors(viewport);
    context->SetRenderTarget(dst);

    if (checkIfSkipPass())
    {
        context->Draw(src);
    }
    else
    {
        Data data;
        data.TexelSize.X = 1.0f / (float)src->Width();
        data.TexelSize.Y = 1.0f / (float)src->Height();
        auto cb = _shader->GetShader()->GetCB(0);
        context->UpdateCB(cb, &data);
        context->BindCB(0, cb);
        context->BindSR(0, src);
        context->SetState(_psUpscale);
        context->DrawFullscreenTriangle();
        context->UnBindCB(0);
    }

    context->ResetRenderTarget();
}
