#include "adapters/windows/rendering/Dx11Backend.h"
#include "adapters/windows/rendering/RenderDiagnostics.h"
#include "application/lifecycle/CallbackBarrier.h"

#include "adapters/windows/rendering/ImGuiHost.h"
#include "adapters/windows/rendering/WindowHook.h"

#include "foundation/diagnostics/Logger.h"
#include "foundation/diagnostics/CrashDiagnostics.h"
#include "application/lifecycle/RaidContext.h"
#include "external/detours/detours.h"
#include "application/lifecycle/RuntimeState.h"

#include <d3d11.h>
#include <chrono>
#include <dxgi.h>

#include <cstdint>

#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d11.lib")

namespace
{
    using PresentFn = HRESULT(__stdcall*)(IDXGISwapChain*, UINT, UINT);

    constexpr int kPresentIndex = 8;

    PresentFn g_original_present = nullptr;
    ID3D11Device* g_device = nullptr;
    ID3D11DeviceContext* g_context = nullptr;
    ID3D11RenderTargetView* g_render_target = nullptr;
    HWND g_window = nullptr;
    bool g_hooked = false;
    bool g_frame_initialized = false;
    ta::application::IImGuiClient* g_client = nullptr;

    template <typename T>
    void release_com(T*& value) noexcept
    {
        if (value)
        {
            value->Release();
            value = nullptr;
        }
    }

    HWND create_dummy_window(HMODULE module) noexcept
    {
        WNDCLASSEXW window_class{};
        window_class.cbSize = sizeof(window_class);
        window_class.style = CS_HREDRAW | CS_VREDRAW;
        window_class.lpfnWndProc = DefWindowProcW;
        window_class.hInstance = module;
        window_class.lpszClassName = L"TarkovAddonsRefactoredDx11DummyWindow";

        if (!RegisterClassExW(&window_class))
            return nullptr;

        HWND window = CreateWindowExW(
            0,
            window_class.lpszClassName,
            L"TarkovAddonsRefactoredDx11DummyWindow",
            WS_OVERLAPPEDWINDOW,
            0,
            0,
            100,
            100,
            nullptr,
            nullptr,
            module,
            nullptr);

        UnregisterClassW(window_class.lpszClassName, module);
        return window;
    }

    void release_render_target() noexcept
    {
        release_com(g_render_target);
    }

    bool create_render_target(IDXGISwapChain* swap_chain) noexcept
    {
        release_render_target();

        ID3D11Texture2D* back_buffer = nullptr;
        const HRESULT buffer_result = swap_chain->GetBuffer(0, __uuidof(ID3D11Texture2D),
                                                            reinterpret_cast<void**>(&back_buffer));
        if (FAILED(buffer_result) || !back_buffer)
            return false;

        const HRESULT view_result = g_device->CreateRenderTargetView(back_buffer, nullptr, &g_render_target);
        back_buffer->Release();
        return SUCCEEDED(view_result) && g_render_target;
    }

    bool init_frame_objects(IDXGISwapChain* swap_chain) noexcept
    {
        if (g_frame_initialized)
            return true;

        if (!swap_chain)
            return false;

        if (FAILED(swap_chain->GetDevice(__uuidof(ID3D11Device), reinterpret_cast<void**>(&g_device))) || !g_device)
            return false;

        g_device->GetImmediateContext(&g_context);
        if (!g_context)
            return false;

        DXGI_SWAP_CHAIN_DESC swap_desc{};
        if (FAILED(swap_chain->GetDesc(&swap_desc)) || !swap_desc.OutputWindow)
            return false;

        g_window = swap_desc.OutputWindow;

        if (!create_render_target(swap_chain))
            return false;

        if (!ta::adapters::windows::rendering::window_hook::install(g_window))
            ta::foundation::logger::warn("DX11 backend failed to install WindowHook; rendering will continue without input capture");

        if (!g_client || !ta::adapters::windows::rendering::imgui_host::init(g_window, g_device, g_context, *g_client))
            return false;

        g_frame_initialized = true;
        return true;
    }

    HRESULT __stdcall hooked_present(IDXGISwapChain* swap_chain, UINT sync_interval, UINT flags)
    {
        ta::foundation::diagnostics::crash::CallbackScope diagnostic_scope(
            "render.present.enter",
            reinterpret_cast<std::uintptr_t>(swap_chain),
            reinterpret_cast<std::uintptr_t>(g_original_present),
            reinterpret_cast<std::uintptr_t>(g_context));
        ta::foundation::diagnostics::crash::set_in_raid(
            ta::application::raid_context::raw_in_raid());
        ta::application::runtime::CallbackGuard callback_guard;
        ta::adapters::windows::rendering::diagnostics::mark_present();

        if (!g_original_present)
        {
            ta::foundation::diagnostics::crash::set_stage("render.present.missing-original");
            return DXGI_ERROR_INVALID_CALL;
        }

        if (!ta::application::runtime::should_accept_callbacks())
        {
            ta::foundation::diagnostics::crash::set_stage(
                "render.present.original-only",
                reinterpret_cast<std::uintptr_t>(swap_chain),
                reinterpret_cast<std::uintptr_t>(g_original_present));
            return g_original_present(swap_chain, sync_interval, flags);
        }

        ta::foundation::diagnostics::crash::set_stage(
            "render.present.init-frame",
            reinterpret_cast<std::uintptr_t>(swap_chain),
            reinterpret_cast<std::uintptr_t>(g_device),
            reinterpret_cast<std::uintptr_t>(g_context));
        if (init_frame_objects(swap_chain) && g_context && g_render_target && ta::adapters::windows::rendering::imgui_host::initialized())
        {
            const auto overlay_started = std::chrono::steady_clock::now();
            ta::foundation::diagnostics::crash::set_stage("render.present.imgui-new-frame");
            ta::adapters::windows::rendering::imgui_host::new_frame();
            ta::foundation::diagnostics::crash::set_stage("render.present.imgui-draw");
            ta::adapters::windows::rendering::imgui_host::draw();
            ta::foundation::diagnostics::crash::set_stage(
                "render.present.bind-target",
                reinterpret_cast<std::uintptr_t>(g_context),
                reinterpret_cast<std::uintptr_t>(g_render_target));
            g_context->OMSetRenderTargets(1, &g_render_target, nullptr);
            ta::foundation::diagnostics::crash::set_stage("render.present.imgui-render");
            ta::adapters::windows::rendering::imgui_host::render_draw_data();
            ta::foundation::diagnostics::crash::set_stage("render.present.metrics");
            const auto overlay_elapsed = std::chrono::steady_clock::now() - overlay_started;
            ta::adapters::windows::rendering::diagnostics::record_overlay_frame_duration(
                static_cast<std::uint64_t>(
                    std::chrono::duration_cast<std::chrono::nanoseconds>(overlay_elapsed).count()));
        }

        ta::foundation::diagnostics::crash::set_stage(
            "render.present.original",
            reinterpret_cast<std::uintptr_t>(swap_chain),
            reinterpret_cast<std::uintptr_t>(g_original_present));
        const HRESULT result = g_original_present(swap_chain, sync_interval, flags);
        ta::foundation::diagnostics::crash::set_stage("render.present.complete");
        return result;
    }
}

namespace ta::adapters::windows::rendering::dx11_backend
{
    bool install(HMODULE module, ta::application::IImGuiClient& client) noexcept
    {
        if (g_hooked)
            return true;

        g_client = &client;

        if (!GetModuleHandleW(L"d3d11.dll") || !GetModuleHandleW(L"dxgi.dll"))
        {
            g_client = nullptr;
            return false;
        }

        HWND dummy_window = create_dummy_window(module);
        if (!dummy_window)
        {
            ta::foundation::logger::error("DX11 backend failed to create dummy window");
            g_client = nullptr;
            return false;
        }

        DXGI_SWAP_CHAIN_DESC swap_desc{};
        swap_desc.BufferCount = 1;
        swap_desc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
        swap_desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        swap_desc.OutputWindow = dummy_window;
        swap_desc.SampleDesc.Count = 1;
        swap_desc.Windowed = TRUE;
        swap_desc.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

        constexpr D3D_FEATURE_LEVEL feature_levels[] = {
            D3D_FEATURE_LEVEL_11_0,
            D3D_FEATURE_LEVEL_10_1,
        };

        IDXGISwapChain* dummy_swap_chain = nullptr;
        ID3D11Device* dummy_device = nullptr;
        ID3D11DeviceContext* dummy_context = nullptr;
        D3D_FEATURE_LEVEL feature_level{};

        const HRESULT device_result = D3D11CreateDeviceAndSwapChain(
            nullptr,
            D3D_DRIVER_TYPE_HARDWARE,
            nullptr,
            D3D11_CREATE_DEVICE_BGRA_SUPPORT,
            feature_levels,
            static_cast<UINT>(sizeof(feature_levels) / sizeof(feature_levels[0])),
            D3D11_SDK_VERSION,
            &swap_desc,
            &dummy_swap_chain,
            &dummy_device,
            &feature_level,
            &dummy_context);

        if (FAILED(device_result) || !dummy_swap_chain)
        {
            ta::foundation::logger::error("DX11 backend failed to create dummy device and swap chain");
            release_com(dummy_context);
            release_com(dummy_device);
            release_com(dummy_swap_chain);
            DestroyWindow(dummy_window);
            g_client = nullptr;
            return false;
        }

        auto** vtable = *reinterpret_cast<void***>(dummy_swap_chain);
        g_original_present = reinterpret_cast<PresentFn>(vtable[kPresentIndex]);

        LONG detour_result = DetourTransactionBegin();
        if (detour_result == NO_ERROR)
            detour_result = DetourUpdateThread(GetCurrentThread());
        if (detour_result == NO_ERROR)
            detour_result = DetourAttach(
                reinterpret_cast<PVOID*>(&g_original_present),
                reinterpret_cast<PVOID>(hooked_present));

        if (detour_result == NO_ERROR)
            detour_result = DetourTransactionCommit();
        else
            DetourTransactionAbort();

        release_com(dummy_context);
        release_com(dummy_device);
        release_com(dummy_swap_chain);
        DestroyWindow(dummy_window);

        if (detour_result != NO_ERROR)
        {
            ta::foundation::logger::error("DX11 backend failed to hook Present");
            g_original_present = nullptr;
            g_client = nullptr;
            return false;
        }

        g_hooked = true;
        return true;
    }

    bool detach() noexcept
    {
        if (!g_hooked)
            return true;
        if (!g_original_present)
            return false;

        if (DetourTransactionBegin() != NO_ERROR)
            return false;
        if (DetourUpdateThread(GetCurrentThread()) != NO_ERROR)
        {
            DetourTransactionAbort();
            return false;
        }
        if (DetourDetach(reinterpret_cast<PVOID*>(&g_original_present),
                         reinterpret_cast<PVOID>(hooked_present)) != NO_ERROR)
        {
            DetourTransactionAbort();
            return false;
        }
        if (DetourTransactionCommit() != NO_ERROR)
            return false;

        g_hooked = false;
        return true;
    }

    void shutdown_resources() noexcept
    {
        ta::adapters::windows::rendering::imgui_host::shutdown();
        ta::adapters::windows::rendering::window_hook::uninstall();
        release_render_target();
        release_com(g_context);
        release_com(g_device);

        g_window = nullptr;
        if (!g_hooked)
            g_original_present = nullptr;
        g_frame_initialized = false;
        g_client = nullptr;
    }

}
