/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
*/

// Regression test for issue #89: Device::PreferDirectDeviceWrite is the runtime
// upload-strategy helper that decides whether a CPU-written, GPU-read buffer
// should be written directly into a HOST_VISIBLE | DEVICE_LOCAL allocation
// (true) or staged through a HOST_VISIBLE buffer + vkCmdCopyBuffer into pure
// DEVICE_LOCAL (false). The choice is platform-dependent:
//   - ReBAR / UMA: the host-visible device-local heap is ~all of VRAM, so
//     staging is pure overhead -> direct.
//   - non-ReBAR discrete: HOST_VISIBLE | DEVICE_LOCAL is a small BAR window on
//     a separate heap; large buffers must stage (#58), small/hot ones may map.
//   - no host-visible device-local type at all -> staging is mandatory.
//
// The decision is pure CPU logic over Device::memoryProperties (both the
// memory types and the heaps), so this test installs synthetic layouts and
// drives it directly — no GPU device needed at runtime, mirroring
// MemoryTypeFallback.

#include <cstdlib>
#include "vulkan/vulkan.h"

import Crafter.Graphics;
import std;
using namespace Crafter;

namespace {

int failures = 0;

void Check(bool ok, std::string_view what) {
    std::println("{}  {}", ok ? "PASS" : "FAIL", what);
    if (!ok) ++failures;
}

constexpr auto DEVICE_LOCAL  = VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
constexpr auto HOST_VISIBLE  = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
constexpr auto HOST_COHERENT = VK_MEMORY_PROPERTY_HOST_COHERENT_BIT;
constexpr auto HOST_CACHED   = VK_MEMORY_PROPERTY_HOST_CACHED_BIT;

constexpr VkDeviceSize KiB = 1024;
constexpr VkDeviceSize MiB = 1024 * KiB;
constexpr VkDeviceSize GiB = 1024 * MiB;

struct TypeSpec { VkMemoryPropertyFlags flags; std::uint32_t heapIndex; };
struct HeapSpec { VkDeviceSize size; VkMemoryHeapFlags flags; };

// Install a synthetic memory layout: explicit types (with their heap index)
// and explicit heaps (size + flags).
void SetMemory(std::initializer_list<TypeSpec> types,
               std::initializer_list<HeapSpec> heaps) {
    Device::memoryProperties = {};
    Device::memoryProperties.memoryTypeCount = static_cast<std::uint32_t>(types.size());
    std::uint32_t i = 0;
    for (const TypeSpec& t : types) {
        Device::memoryProperties.memoryTypes[i].propertyFlags = t.flags;
        Device::memoryProperties.memoryTypes[i].heapIndex = t.heapIndex;
        ++i;
    }
    Device::memoryProperties.memoryHeapCount = static_cast<std::uint32_t>(heaps.size());
    std::uint32_t h = 0;
    for (const HeapSpec& s : heaps) {
        Device::memoryProperties.memoryHeaps[h].size = s.size;
        Device::memoryProperties.memoryHeaps[h].flags = s.flags;
        ++h;
    }
    // PreferDirectDeviceWrite reads the cache CacheUploadStrategy derives from
    // memoryProperties (device creation does this once); refresh it here since
    // the test mutates the layout directly.
    Device::CacheUploadStrategy();
}

constexpr VkMemoryHeapFlags HEAP_DEVICE_LOCAL = VK_MEMORY_HEAP_DEVICE_LOCAL_BIT;

} // namespace

int main() {
    // --- ReBAR: full 24 GiB heap is DEVICE_LOCAL | HOST_VISIBLE (this dev
    //     machine, type 4 on heap 0) -> always direct, even for huge buffers --
    {
        SetMemory(
            { {DEVICE_LOCAL, 0},
              {HOST_VISIBLE | HOST_COHERENT, 1},
              {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 0} },
            { {24 * GiB, HEAP_DEVICE_LOCAL}, {32 * GiB, 0} });
        Check(Device::PreferDirectDeviceWrite(8),
              "ReBAR: a tiny buffer maps directly");
        Check(Device::PreferDirectDeviceWrite(8 * GiB),
              "ReBAR: even a multi-GiB buffer maps directly (no overhead staging)");
    }

    // --- UMA: a single heap with all flags -> direct ----------------------
    {
        SetMemory(
            { {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT | HOST_CACHED, 0} },
            { {125 * GiB, HEAP_DEVICE_LOCAL} });
        Check(Device::PreferDirectDeviceWrite(64 * GiB),
              "UMA: single all-flags heap maps directly");
    }

    // --- non-ReBAR discrete: small 256 MiB BAR window (heap 1) separate from
    //     an 8 GiB VRAM heap (heap 0) -> stage large, map small ------------
    {
        SetMemory(
            { {DEVICE_LOCAL, 0},                                   // pure VRAM
              {HOST_VISIBLE | HOST_COHERENT, 2},                   // system RAM
              {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} },  // BAR window
            { {8 * GiB, HEAP_DEVICE_LOCAL},                        // heap 0: VRAM
              {256 * MiB, HEAP_DEVICE_LOCAL},                      // heap 1: BAR
              {32 * GiB, 0} });                                    // heap 2: RAM
        Check(!Device::PreferDirectDeviceWrite(128 * MiB),
              "small BAR: a large buffer stages (would exhaust the 256 MiB window)");
        Check(Device::PreferDirectDeviceWrite(8 * MiB),
              "small BAR: a small/hot buffer maps directly into the window");
        // Budget is 1/8 of the window = 32 MiB. Boundary checks.
        Check(Device::PreferDirectDeviceWrite(32 * MiB),
              "small BAR: a buffer exactly at the per-buffer budget still maps");
        Check(!Device::PreferDirectDeviceWrite(32 * MiB + 1),
              "small BAR: one byte over the budget stages");
    }

    // --- no host-visible device-local type (no resizable BAR) -> must stage -
    {
        SetMemory(
            { {DEVICE_LOCAL, 0},
              {HOST_VISIBLE | HOST_COHERENT, 1},
              {HOST_VISIBLE | HOST_COHERENT | HOST_CACHED, 1} },
            { {8 * GiB, HEAP_DEVICE_LOCAL}, {32 * GiB, 0} });
        Check(!Device::PreferDirectDeviceWrite(8),
              "no DEVICE_LOCAL|HOST_VISIBLE type -> staging is mandatory even for a tiny buffer");
    }

    // --- 90% threshold: a BAR heap that is most-but-not-all of VRAM counts as
    //     ReBAR; one well below the threshold is treated as a window --------
    {
        // BAR heap = 9.5 GiB, VRAM = 10 GiB -> 95% >= 90% -> ReBAR -> direct.
        SetMemory(
            { {DEVICE_LOCAL, 0},
              {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} },
            { {10 * GiB, HEAP_DEVICE_LOCAL}, {VkDeviceSize(9.5 * GiB), HEAP_DEVICE_LOCAL} });
        Check(Device::PreferDirectDeviceWrite(4 * GiB),
              "BAR >= 90% of VRAM is treated as ReBAR: large buffer still maps");

        // BAR heap = 2 GiB, VRAM = 10 GiB -> 20% < 90% -> window -> stage big.
        SetMemory(
            { {DEVICE_LOCAL, 0},
              {DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT, 1} },
            { {10 * GiB, HEAP_DEVICE_LOCAL}, {2 * GiB, HEAP_DEVICE_LOCAL} });
        Check(!Device::PreferDirectDeviceWrite(4 * GiB),
              "BAR well below 90% of VRAM is a window: large buffer stages");
        Check(Device::PreferDirectDeviceWrite(128 * MiB),
              "BAR window of 2 GiB still maps a small buffer (budget = 256 MiB)");
    }

    if (failures != 0) {
        std::println("{} check(s) failed", failures);
        return EXIT_FAILURE;
    }
    std::println("all checks passed");
    return EXIT_SUCCESS;
}
