#include "device.hpp"

#include "runtime/config.h"

static RT_CVAR_S(rt_Dx11AdapterName, "Name of the desired adapter (GPU). Default: Empty.", "");

rt_dx11_device::rt_dx11_device() : m_is_initialized(false) {
}

rt_dx11_device::~rt_dx11_device() {
}

rt_result rt_dx11_device::Initialize(const rt_renderer_init_info *info) {
    // Create the necessary objects (Device, SwapChain, Immediate Context)
    HRESULT hr = S_OK;
    ComPtr<IDXGIFactory1> factory;
    if (FAILED(hr = CreateDXGIFactory1(IID_PPV_ARGS(&factory)))) {
        rtReportError("DX11", "CreateDXGIFactory1 failed with %d", hr);
        return RT_UNKNOWN_ERROR;
    }
    if (FAILED(hr = factory.As(&m_dxgi_factory))) {
        rtReportError("DX11", "Failed to retrieve IDXGIFactory4 with %d", hr);
        return RT_UNKNOWN_ERROR;
    }

    // Retrieve the selected adapter
    {
        ComPtr<IDXGIAdapter1> adapter = nullptr;
        WCHAR w_name[128];
        memset(w_name, 0, sizeof(w_name));
        if (rtUTF8ToWStr(rt_Dx11AdapterName.s, w_name, RT_ARRAY_COUNT(w_name)) != RT_SUCCESS) {
            rtLog("DX11",
                  "The provided adapter name does not fit into the statically sized array.");

            if (FAILED(m_dxgi_factory->EnumAdapters1(0, &adapter))) {
                rtReportError("DX11", "Failed to retrieve the default adapter.");
                return RT_UNKNOWN_ERROR;
            }
            if (FAILED(hr = adapter.As(&m_adapter))) {
                rtReportError("DX11", "Failed to retrieve IDXGIAdapter3 with %d", hr);
                return RT_UNKNOWN_ERROR;
            }
        }

        UINT i = 0;

        while (m_dxgi_factory->EnumAdapters1(i, &adapter) == S_OK) {
            DXGI_ADAPTER_DESC1 desc;
            if (FAILED(adapter->GetDesc1(&desc))) {
                ++i;
                adapter->Release();
                continue;
            }

            if (memcmp(desc.Description, w_name, sizeof(w_name)) == 0) {
                if (FAILED(hr = adapter.As(&m_adapter))) {
                    rtReportError("DX11", "Failed to retrieve IDXGIAdapter3 with %d", hr);
                    return RT_UNKNOWN_ERROR;
                }
                break;
            }
            ++i;
        }

        if (!m_adapter.Get()) {
            if (FAILED(m_dxgi_factory->EnumAdapters1(0, &adapter))) {
                rtReportError("DX11", "Failed to retrieve the default adapter.");
                return RT_UNKNOWN_ERROR;
            }
            if (FAILED(hr = adapter.As(&m_adapter))) {
                rtReportError("DX11", "Failed to retrieve IDXGIAdapter3 with %d", hr);
                return RT_UNKNOWN_ERROR;
            }
        }
    }

    {
        // Get monitor properties
        ComPtr<IDXGIOutput> output;

        HMONITOR wnd_mon = MonitorFromWindow(info->hWnd, MONITOR_DEFAULTTOPRIMARY);
        if (wnd_mon) {
            UINT i = 0;
            while (m_adapter->EnumOutputs(i, &output) == S_OK) {
                DXGI_OUTPUT_DESC desc = {};
                output->GetDesc(&desc);
                if (desc.Monitor == wnd_mon) {
                    GetDisplayInfo(output.Get());
                    break;
                }
                ++i;
            }
        } else {
            rtLog("DX11", "MonitorFromWindow returned a non-valid monitor.");
            if (!FAILED(m_adapter->EnumOutputs(0, &output))) {
                GetDisplayInfo(output.Get());
            }
        }
    }

    UINT device_flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;
#ifdef RT_DEBUG
    device_flags |= D3D11_CREATE_DEVICE_DEBUGGABLE | D3D11_CREATE_DEVICE_DEBUG;
#endif

    // Check for tearing support, which is a requirement for variable refresh displays
    if (FAILED(m_dxgi_factory->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING,
                                                   &m_tearing_supported,
                                                   sizeof(m_tearing_supported))))
        m_tearing_supported = false;

    D3D_FEATURE_LEVEL feature_levels[] = {D3D_FEATURE_LEVEL_11_1};

    ComPtr<ID3D11Device> device;
    ComPtr<ID3D11DeviceContext> context;
    if (FAILED(hr = D3D11CreateDevice(m_adapter.Get(),
                                      D3D_DRIVER_TYPE_HARDWARE,
                                      NULL,
                                      device_flags,
                                      feature_levels,
                                      RT_ARRAY_COUNT(feature_levels),
                                      D3D11_SDK_VERSION,
                                      &device,
                                      nullptr,
                                      &context))) {
        rtLog("DX11", "D3D11CreateDevice failed with %d. Retrying without DEBUGGABLE flag...");
        device_flags &= ~D3D11_CREATE_DEVICE_DEBUGGABLE;
        if (FAILED(hr = D3D11CreateDevice(nullptr, // Use the default adapter TODO: Select
                                          D3D_DRIVER_TYPE_HARDWARE,
                                          NULL,
                                          device_flags,
                                          feature_levels,
                                          RT_ARRAY_COUNT(feature_levels),
                                          D3D11_SDK_VERSION,
                                          &device,
                                          nullptr,
                                          &context))) {
            rtReportError("DX11", "D3D11CreateDevice failed with %d", hr);
            return RT_UNKNOWN_ERROR;
        }
    }
    if (FAILED(hr = device.As(&m_device))) {
        rtReportError("DX11", "Failed to retrieve ID3D11Device5 with %d", hr);
        return RT_UNKNOWN_ERROR;
    }
    if (FAILED(hr = context.As(&m_context))) {
        rtReportError("DX11", "Failed to retrieve ID3D11DeviceContext3 with %d", hr);
        return RT_UNKNOWN_ERROR;
    }

    if (info->is_fullscreen) {
        // Don't allow Alt+Enter to switch the window to fullscreen exclusive
        m_dxgi_factory->MakeWindowAssociation(info->hWnd, DXGI_MWA_NO_ALT_ENTER);
    }

    UINT swap_chain_flags = 0;
    if (m_tearing_supported)
        swap_chain_flags |= DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING;

    // TODO(Kevin): HDR Support
    DXGI_SWAP_CHAIN_DESC1 swap_chain_desc = {};
    swap_chain_desc.Width                 = info->width;
    swap_chain_desc.Height                = info->height;
    swap_chain_desc.Format                = DXGI_FORMAT_B8G8R8A8_UNORM;
    swap_chain_desc.Stereo                = FALSE;
    swap_chain_desc.SampleDesc.Count      = 1;
    swap_chain_desc.BufferUsage           = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    swap_chain_desc.BufferCount           = 2;
    swap_chain_desc.SwapEffect            = DXGI_SWAP_EFFECT_FLIP_DISCARD;
    swap_chain_desc.Scaling               = DXGI_SCALING_STRETCH;
    swap_chain_desc.AlphaMode             = DXGI_ALPHA_MODE_IGNORE;
    swap_chain_desc.Flags                 = swap_chain_flags;

    if (FAILED(hr = m_dxgi_factory->CreateSwapChainForHwnd(m_device.Get(),
                                                           info->hWnd,
                                                           &swap_chain_desc,
                                                           nullptr,
                                                           nullptr,
                                                           &m_swap_chain))) {
        rtReportError("DX11", "CreateSwapChainForHwnd failed with %d", hr);
        return RT_UNKNOWN_ERROR;
    }

    m_is_initialized = true;
    return RT_SUCCESS;
}

void rt_dx11_device::Shutdown(void) {
    m_device.Reset();
    m_is_initialized = false;
}

void rt_dx11_device::GetDisplayInfo(IDXGIOutput *output) {
    // TODO Not implemented yet
    m_monitor_refresh_rate = 1.f / 60.f;
}