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

#if GRAPHICS_API_DIRECTX12

#include "GPUSwapChainDX12.h"
#include "GPUContextDX12.h"
#include "../IncludeDirectXHeaders.h"
#include "Engine/GraphicsDevice/DirectX/RenderToolsDX.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"

void BackBufferDX12::Setup(GPUSwapChainDX12* window, ID3D12Resource* backbuffer)
{
    // Cache handle and set default initial state for the backbuffers
    initResource(backbuffer, D3D12_RESOURCE_STATE_PRESENT, 1);

    Handle.Init(window, window->GetDevice(), this, GPU_BACK_BUFFER_PIXEL_FORMAT, MSAALevel::None);

    // Create RTV
    {
        D3D12_RENDER_TARGET_VIEW_DESC rtDesc;
        rtDesc.Format = (DXGI_FORMAT)GPU_BACK_BUFFER_PIXEL_FORMAT;
        rtDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
        rtDesc.Texture2D.MipSlice = 0;
        rtDesc.Texture2D.PlaneSlice = 0;
        Handle.SetRTV(rtDesc);
    }

#if GPU_USE_WINDOW_SRV
    // Create SRV
    {
        D3D12_SHADER_RESOURCE_VIEW_DESC srDesc;
        srDesc.Format = (DXGI_FORMAT)GPU_BACK_BUFFER_PIXEL_FORMAT;
        srDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
        srDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
        srDesc.Texture2D.MostDetailedMip = 0;
        srDesc.Texture2D.MipLevels = 1;
        srDesc.Texture2D.ResourceMinLODClamp = 0;
        srDesc.Texture2D.PlaneSlice = 0;
        Handle.SetSRV(srDesc);
    }
#endif
}

void BackBufferDX12::Release()
{
    Handle.Release();
    SAFE_RELEASE(_resource);
}

GPUSwapChainDX12::GPUSwapChainDX12(GPUDeviceDX12* device, Window* window)
    : GPUResourceDX12(device, StringView::Empty)
    , _windowHandle(static_cast<HWND>(window->GetNativePtr()))
    , _swapChain(nullptr)
    , _currentFrameIndex(0)
    , _allowTearing(false)
    , _isFullscreen(false)
{
    ASSERT(_windowHandle);
    _window = window;
}

void GPUSwapChainDX12::OnReleaseGPU()
{
    _device->WaitForGPU();

#if PLATFORM_WINDOWS
    // Disable fullscreen mode
    if (_swapChain)
    {
        VALIDATE_DIRECTX_CALL(_swapChain->SetFullscreenState(false, nullptr));
    }
#endif

    // Release data
    PROFILE_MEM_DEC(Graphics, _memoryUsage);
    releaseBackBuffer();
    _backBuffers.Resize(0);
    if (_swapChain)
    {
        _device->AddResourceToLateRelease(_swapChain);
        _swapChain = nullptr;
    }
    _width = _height = 0;
    _memoryUsage = 0;
}

void GPUSwapChainDX12::releaseBackBuffer()
{
    for (int32 i = 0; i < _backBuffers.Count(); i++)
    {
        _backBuffers[i].Release();
    }
}

bool GPUSwapChainDX12::IsFullscreen()
{
#if PLATFORM_XBOX_SCARLETT || PLATFORM_XBOX_ONE
    return true;
#else
    // Check if has no swap chain created
    if (_swapChain == nullptr)
        return false;

    // Get state
    BOOL state;
    VALIDATE_DIRECTX_CALL(_swapChain->GetFullscreenState(&state, nullptr));
    return state == TRUE;
#endif
}

void GPUSwapChainDX12::SetFullscreen(bool isFullscreen)
{
#if PLATFORM_WINDOWS
    if (_swapChain && isFullscreen != IsFullscreen())
    {
        _device->WaitForGPU();
        GPUDeviceLock lock(_device);

        DXGI_SWAP_CHAIN_DESC swapChainDesc;
        _swapChain->GetDesc(&swapChainDesc);

        // Setup target for fullscreen mode
        IDXGIOutput* output = nullptr;
        if (isFullscreen && _device->Outputs.HasItems())
        {
            const uint32 outputIdx = 0;
            auto& outputDX = _device->Outputs[outputIdx];
            output = outputDX.Output.Get();
            swapChainDesc.BufferDesc = outputDX.DesktopViewMode;
        }

        if (FAILED(_swapChain->ResizeTarget(&swapChainDesc.BufferDesc)))
        {
            LOG(Warning, "Swapchain resize failed.");
        }

        if (FAILED(_swapChain->SetFullscreenState(isFullscreen, output)))
        {
            LOG(Warning, "Cannot change fullscreen mode for '{0}' to {1}.", ToString(), isFullscreen);
        }

        _isFullscreen = isFullscreen;

        // Buffers must be resized in flip presentation model
        if (swapChainDesc.SwapEffect == DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL ||
            swapChainDesc.SwapEffect == DXGI_SWAP_EFFECT_FLIP_DISCARD)
        {
            const int32 width = _width;
            const int32 height = _height;
            _width = _height = 0;
            Resize(width, height);
        }
    }
#else
    LOG(Info, "Cannot change fullscreen mode on this platform");
#endif
}

bool GPUSwapChainDX12::Resize(int32 width, int32 height)
{
    // Check if window state won't change
    if (width == _width && height == _height)
    {
        return false;
    }

    _device->WaitForGPU();
    GPUDeviceLock lock(_device);
    _allowTearing = _device->AllowTearing;
    _format = GPU_BACK_BUFFER_PIXEL_FORMAT;

#if PLATFORM_XBOX_SCARLETT || PLATFORM_XBOX_ONE
    ReleaseGPU();

    _currentFrameIndex = 0;
    _width = width;
    _height = height;
    _memoryUsage = RenderTools::CalculateTextureMemoryUsage(_format, _width, _height, 1) * DX12_BACK_BUFFER_COUNT;

    getBackBuffer();
#else
    DXGI_SWAP_CHAIN_DESC1 swapChainDesc;
    if (_swapChain == nullptr)
    {
        ReleaseGPU();

        // Create swap chain description
        swapChainDesc.Width = width;
        swapChainDesc.Height = height;
        swapChainDesc.Format = RenderToolsDX::ToDxgiFormat(_format);
        swapChainDesc.Stereo = FALSE;
        swapChainDesc.SampleDesc.Count = 1;
        swapChainDesc.SampleDesc.Quality = 0;
        swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        swapChainDesc.BufferCount = DX12_BACK_BUFFER_COUNT;
        swapChainDesc.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
        swapChainDesc.Scaling = DXGI_SCALING_STRETCH;
        swapChainDesc.AlphaMode = DXGI_ALPHA_MODE_UNSPECIFIED;
        swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
#if GPU_USE_WINDOW_SRV
        swapChainDesc.BufferUsage |= DXGI_USAGE_SHADER_INPUT;
#endif
        if (_allowTearing)
            swapChainDesc.Flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;
        DXGI_SWAP_CHAIN_FULLSCREEN_DESC fullscreenDesc;
        if (_device->Outputs.HasItems())
        {
            const uint32 outputIdx = 0;
            auto& output = _device->Outputs[outputIdx];
            fullscreenDesc.RefreshRate = output.DesktopViewMode.RefreshRate;
            fullscreenDesc.Scaling = output.DesktopViewMode.Scaling;
            fullscreenDesc.ScanlineOrdering = output.DesktopViewMode.ScanlineOrdering;
            fullscreenDesc.Windowed = TRUE;
        }
        else
        {
            fullscreenDesc.RefreshRate.Numerator = 0;
            fullscreenDesc.RefreshRate.Denominator = 1;
            fullscreenDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
            fullscreenDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
            fullscreenDesc.Windowed = TRUE;
        }

        // Create swap chain (it needs the queue so that it can force a flush on it)
        IDXGISwapChain1* swapChain;
        auto dxgiFactory = _device->GetDXGIFactory();
        VALIDATE_DIRECTX_CALL(dxgiFactory->CreateSwapChainForHwnd(_device->GetCommandQueueDX12(), _windowHandle, &swapChainDesc, &fullscreenDesc, nullptr, &swapChain));
        _swapChain = static_cast<IDXGISwapChain3*>(swapChain);
        ASSERT(_swapChain);
        DX_SET_DEBUG_NAME_EX(_swapChain, TEXT("RenderOutput"), TEXT("SwapChain"), TEXT(""));
        _swapChain->SetBackgroundColor((const DXGI_RGBA*)Color::Black.Raw);
        _backBuffers.Resize(swapChainDesc.BufferCount);

        // Disable DXGI changes to the window
        VALIDATE_DIRECTX_CALL(dxgiFactory->MakeWindowAssociation(_windowHandle, DXGI_MWA_NO_ALT_ENTER));
    }
    else
    {
        releaseBackBuffer();

        _swapChain->GetDesc1(&swapChainDesc);

        VALIDATE_DIRECTX_CALL(_swapChain->ResizeBuffers(swapChainDesc.BufferCount, width, height, swapChainDesc.Format, swapChainDesc.Flags));
    }

    _currentFrameIndex = _swapChain->GetCurrentBackBufferIndex();
    _width = width;
    _height = height;
    _memoryUsage = RenderTools::CalculateTextureMemoryUsage(_format, _width, _height, 1) * swapChainDesc.BufferCount;
    PROFILE_MEM_INC(Graphics, _memoryUsage);

    getBackBuffer();
#endif

    return false;
}

void GPUSwapChainDX12::CopyBackbuffer(GPUContext* context, GPUTexture* dst)
{
    const auto contextDX12 = (GPUContextDX12*)context;

    auto dstDX12 = (GPUTextureDX12*)dst;
    auto backbuffer = &_backBuffers[_currentFrameIndex];

    contextDX12->SetResourceState(dstDX12, D3D12_RESOURCE_STATE_COPY_DEST);
    contextDX12->SetResourceState(backbuffer, D3D12_RESOURCE_STATE_COPY_SOURCE);
    contextDX12->FlushResourceBarriers();

    if (dst->IsStaging())
    {
        const int32 copyOffset = dstDX12->ComputeBufferOffset(0, D3D12_TEXTURE_DATA_PITCH_ALIGNMENT, D3D12_TEXTURE_DATA_PLACEMENT_ALIGNMENT);

        D3D12_TEXTURE_COPY_LOCATION dstLocation;
        dstLocation.pResource = dstDX12->GetResource();
        dstLocation.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT;
        dstLocation.PlacedFootprint.Offset = copyOffset;
        dstLocation.PlacedFootprint.Footprint.Width = dstDX12->CalculateMipSize(dstDX12->Width(), 0);
        dstLocation.PlacedFootprint.Footprint.Height = dstDX12->CalculateMipSize(dstDX12->Height(), 0);
        dstLocation.PlacedFootprint.Footprint.Depth = dstDX12->CalculateMipSize(dstDX12->Depth(), 0);
        dstLocation.PlacedFootprint.Footprint.Format = RenderToolsDX::ToDxgiFormat(dstDX12->Format());
        dstLocation.PlacedFootprint.Footprint.RowPitch = dstDX12->ComputeRowPitch(0, D3D12_TEXTURE_DATA_PITCH_ALIGNMENT);

        D3D12_TEXTURE_COPY_LOCATION srcLocation;
        srcLocation.pResource = backbuffer->GetResource();
        srcLocation.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX;
        srcLocation.SubresourceIndex = 0;

        contextDX12->GetCommandList()->CopyTextureRegion(&dstLocation, 0, 0, 0, &srcLocation, nullptr);
    }
    else
    {
        contextDX12->GetCommandList()->CopyResource(dstDX12->GetResource(), backbuffer->GetResource());
    }
}

void GPUSwapChainDX12::getBackBuffer()
{
    _backBuffers.Resize(DX12_BACK_BUFFER_COUNT);
    for (int32 i = 0; i < _backBuffers.Count(); i++)
    {
        ID3D12Resource* backbuffer;
#if PLATFORM_XBOX_SCARLETT || PLATFORM_XBOX_ONE
        D3D12_HEAP_PROPERTIES swapChainHeapProperties;
        swapChainHeapProperties.Type = D3D12_HEAP_TYPE_DEFAULT;
        swapChainHeapProperties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
        swapChainHeapProperties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
        swapChainHeapProperties.CreationNodeMask = 1;
        swapChainHeapProperties.VisibleNodeMask = 1;
        D3D12_RESOURCE_DESC swapChainBufferDesc;
        swapChainBufferDesc.MipLevels = 1;
        swapChainBufferDesc.Format = RenderToolsDX::ToDxgiFormat(_format);
        swapChainBufferDesc.Width = _width;
        swapChainBufferDesc.Height = _height;
        swapChainBufferDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET;
        swapChainBufferDesc.DepthOrArraySize = 1;
        swapChainBufferDesc.SampleDesc.Count = 1;
        swapChainBufferDesc.SampleDesc.Quality = 0;
        swapChainBufferDesc.Alignment = 0;
        swapChainBufferDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
        swapChainBufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
        D3D12_CLEAR_VALUE swapChainOptimizedClearValue = {};
        swapChainOptimizedClearValue.Format = swapChainBufferDesc.Format;
        VALIDATE_DIRECTX_CALL(_device->GetDevice()->CreateCommittedResource(
            &swapChainHeapProperties,
            D3D12_HEAP_FLAG_ALLOW_DISPLAY,
            &swapChainBufferDesc,
            D3D12_RESOURCE_STATE_PRESENT,
            &swapChainOptimizedClearValue,
            IID_GRAPHICS_PPV_ARGS(&backbuffer)));
#else
        VALIDATE_DIRECTX_CALL(_swapChain->GetBuffer(i, IID_PPV_ARGS(&backbuffer)));
#endif
        DX_SET_DEBUG_NAME_EX(backbuffer, TEXT("RenderOutput"), TEXT("BackBuffer"), i);
        _backBuffers[i].Setup(this, backbuffer);
    }
}

#if PLATFORM_XBOX_SCARLETT || PLATFORM_XBOX_ONE

void GPUSwapChainDX12::Begin(RenderTask* task)
{
    // Wait until frame start is signaled
    _framePipelineToken = D3D12XBOX_FRAME_PIPELINE_TOKEN_NULL;
    VALIDATE_DIRECTX_CALL(_device->GetDevice()->WaitFrameEventX(D3D12XBOX_FRAME_EVENT_ORIGIN, INFINITE, nullptr, D3D12XBOX_WAIT_FRAME_EVENT_FLAG_NONE, &_framePipelineToken));

    GPUSwapChain::Begin(task);
}

#endif

void GPUSwapChainDX12::End(RenderTask* task)
{
    GPUSwapChain::End(task);

    auto context = _device->GetMainContextDX12();

    // Indicate that the back buffer will be used to present a frame
    // Note: after that we should not use this backbuffer
    context->SetResourceState(&_backBuffers[_currentFrameIndex], D3D12_RESOURCE_STATE_PRESENT);

    // Send event
    context->OnSwapChainFlush();
}

void GPUSwapChainDX12::Present(bool vsync)
{
    PROFILE_CPU();
    ZoneColor(TracyWaitZoneColor);
#if PLATFORM_XBOX_SCARLETT || PLATFORM_XBOX_ONE
    ID3D12Resource* backBuffer = _backBuffers[_currentFrameIndex].GetResource();
    D3D12XBOX_PRESENT_PLANE_PARAMETERS planeParameters = {};
    planeParameters.Token = _framePipelineToken;
    planeParameters.ResourceCount = 1;
    planeParameters.ppResources = &backBuffer;
    VALIDATE_DIRECTX_CALL(_device->GetCommandQueueDX12()->PresentX(1, &planeParameters, nullptr));

    // Base
    GPUSwapChain::Present(vsync);

    // Switch to next back buffer
    _currentFrameIndex = (_currentFrameIndex + 1) % DX12_BACK_BUFFER_COUNT;
#else
    // Present frame
    UINT presentFlags = 0;
    if (!vsync && !_isFullscreen && _allowTearing)
    {
        presentFlags |= DXGI_PRESENT_ALLOW_TEARING;
    }
    const HRESULT res = _swapChain->Present(vsync ? 1 : 0, presentFlags);
    LOG_DIRECTX_RESULT(res);

    // Base
    GPUSwapChain::Present(vsync);

    // Switch to next back buffer
    _currentFrameIndex = _swapChain->GetCurrentBackBufferIndex();
#endif
}

GPUTextureView* GPUSwapChainDX12::GetBackBufferView()
{
    return &_backBuffers[_currentFrameIndex].Handle;
}

#endif
