// End-to-end demo of GPU asset decompression via VK_EXT_memory_decompression.
//
// Walks the full compressed-asset pipeline:
//   1. Build a procedural cube + checkerboard texture in memory.
//   2. SaveCompressed → on-disk .cmesh / .ctex (GDeflate streams).
//   3. LoadCompressed* → CompressedMeshAsset / CompressedTextureAsset.
//   4. CPU verification: blob → DecompressCPU → byte-equal to source.
//   5. GPU: Mesh::Build(compressedMesh, cmd) and ImageVulkan::Update(compressedTex, cmd, layout).
//      Picks the GPU path on NVIDIA (extension supported) or the CPU fallback elsewhere.
//
// No rendering — the goal is to exercise the asset pipeline and prove the
// new APIs round-trip. Validation layers are enabled by Crafter.Graphics in
// debug builds, so any VUID violation surfaces during FinishInit().

#include "vulkan/vulkan.h"

import Crafter.Graphics;
import Crafter.Asset;
import Crafter.Math;
import std;

using namespace Crafter;
namespace fs = std::filesystem;

namespace {

// Procedural unit cube centered at origin, scaled so it's visible if the
// example is ever wired into a renderer. 8 unique positions, 36 indices.
MeshAsset<void> MakeCubeMesh() {
    MeshAsset<void> mesh;
    mesh.vertexes = {
        {-50.f, -50.f, -50.f}, { 50.f, -50.f, -50.f},
        { 50.f,  50.f, -50.f}, {-50.f,  50.f, -50.f},
        {-50.f, -50.f,  50.f}, { 50.f, -50.f,  50.f},
        { 50.f,  50.f,  50.f}, {-50.f,  50.f,  50.f},
    };
    mesh.indexes = {
        // -Z
        0, 1, 2,  0, 2, 3,
        // +Z
        4, 6, 5,  4, 7, 6,
        // -X
        0, 3, 7,  0, 7, 4,
        // +X
        1, 5, 6,  1, 6, 2,
        // -Y
        0, 4, 5,  0, 5, 1,
        // +Y
        3, 2, 6,  3, 6, 7,
    };
    return mesh;
}

struct RGBA8 { std::uint8_t r, g, b, a; };

// 256×256 checkerboard with smooth radial fade — compressible enough to make
// the GDeflate ratio interesting, structured enough that the demo is
// meaningful.
TextureAsset<RGBA8> MakeCheckerboard() {
    TextureAsset<RGBA8> tex;
    tex.sizeX = 256;
    tex.sizeY = 256;
    tex.opaque = OpaqueType::FullyOpaque;
    tex.pixels.resize(tex.sizeX * tex.sizeY);
    for (std::uint32_t y = 0; y < tex.sizeY; ++y) {
        for (std::uint32_t x = 0; x < tex.sizeX; ++x) {
            bool checker = ((x / 32) ^ (y / 32)) & 1;
            float dx = float(x) - 128.0f;
            float dy = float(y) - 128.0f;
            float d  = std::sqrt(dx * dx + dy * dy) / 180.0f;
            float fade = std::clamp(1.0f - d, 0.0f, 1.0f);
            std::uint8_t base = checker ? 220 : 40;
            std::uint8_t lit  = static_cast<std::uint8_t>(base * fade + 16);
            tex.pixels[y * tex.sizeX + x] = RGBA8{lit, lit, lit, 255};
        }
    }
    return tex;
}

double Ratio(std::size_t a, std::size_t b) {
    return b == 0 ? 0.0 : 100.0 * double(a) / double(b);
}

} // namespace

int main() {
    const fs::path meshPath = "cube.cmesh";
    const fs::path texPath  = "checker.ctex";

    // ── 1. Build procedural assets ─────────────────────────────────────
    MeshAsset<void>     srcMesh = MakeCubeMesh();
    TextureAsset<RGBA8> srcTex  = MakeCheckerboard();

    const std::size_t srcMeshBytes =
        srcMesh.vertexes.size() * sizeof(srcMesh.vertexes[0])
      + srcMesh.indexes.size()  * sizeof(srcMesh.indexes[0]);
    const std::size_t srcTexBytes  =
        srcTex.pixels.size() * sizeof(RGBA8);

    std::println("Procedural cube:        {} vertices, {} indices ({} bytes)",
                 srcMesh.vertexes.size(), srcMesh.indexes.size(), srcMeshBytes);
    std::println("Procedural checker:     {}x{} RGBA8 ({} bytes)",
                 srcTex.sizeX, srcTex.sizeY, srcTexBytes);

    // ── 2. SaveCompressed ──────────────────────────────────────────────
    srcMesh.SaveCompressed(meshPath);
    srcTex.SaveCompressed(texPath);

    const std::size_t meshFileSize = fs::file_size(meshPath);
    const std::size_t texFileSize  = fs::file_size(texPath);
    std::println("Saved {}: {} bytes ({:.1f}% of raw)",
                 meshPath.string(), meshFileSize, Ratio(meshFileSize, srcMeshBytes));
    std::println("Saved {}: {} bytes ({:.1f}% of raw)",
                 texPath.string(), texFileSize, Ratio(texFileSize, srcTexBytes));

    // ── 3. LoadCompressed ──────────────────────────────────────────────
    CompressedMeshAsset    loadedMesh = LoadCompressedMesh(meshPath);
    CompressedTextureAsset loadedTex  = LoadCompressedTexture(texPath);
    if (loadedMesh.vertexCount != srcMesh.vertexes.size()
        || loadedMesh.indexCount != srcMesh.indexes.size()) {
        std::println(std::cerr,"[FAIL] mesh header mismatch after LoadCompressedMesh");
        return 1;
    }
    if (loadedTex.sizeX != srcTex.sizeX || loadedTex.sizeY != srcTex.sizeY) {
        std::println(std::cerr,"[FAIL] texture header mismatch after LoadCompressedTexture");
        return 1;
    }
    std::println("Loaded headers OK.");

    // ── 4. CPU roundtrip verification ──────────────────────────────────
    {
        std::vector<Vector<float, 3, 3>> v(loadedMesh.vertexCount);
        std::vector<std::uint32_t>       i(loadedMesh.indexCount);
        std::array<std::span<std::byte>, 3> outputs = {
            std::as_writable_bytes(std::span(v)),
            std::as_writable_bytes(std::span(i)),
            std::span<std::byte>{},
        };
        Compression::DecompressCPU(loadedMesh.blob,
            std::span(outputs).first(loadedMesh.blob.regions.size()));
        if (v != srcMesh.vertexes || i != srcMesh.indexes) {
            std::println(std::cerr,"[FAIL] CPU mesh decompress != source");
            return 1;
        }
    }
    {
        std::vector<RGBA8> p(loadedTex.sizeX * loadedTex.sizeY);
        std::array<std::span<std::byte>, 1> outputs = {
            std::as_writable_bytes(std::span(p)),
        };
        Compression::DecompressCPU(loadedTex.blob, outputs);
        if (std::memcmp(p.data(), srcTex.pixels.data(), srcTexBytes) != 0) {
            std::println(std::cerr,"[FAIL] CPU texture decompress != source");
            return 1;
        }
    }
    std::println("CPU roundtrip OK (mesh + texture decode byte-equal).");

    // ── 5. GPU path via Mesh::Build / ImageVulkan::Update ──────────────
    Device::Initialize();
    Window window(800, 600, "Decompression");

    std::println("VK_EXT_memory_decompression: {}",
                 Device::memoryDecompressionSupported ? "AVAILABLE → GPU path" : "absent → CPU fallback");

    VkCommandBuffer cmd = window.StartInit();

    DescriptorHeapVulkan heap;
    heap.Initialize(/*images*/ 1, /*buffers*/ 1, /*samplers*/ 0);
    window.descriptorHeap = &heap;

    Mesh cubeMesh;
    cubeMesh.Build(loadedMesh, cmd);

    ImageVulkan<RGBA8> checkerImage;
    checkerImage.Create(
        loadedTex.sizeX, loadedTex.sizeY, /*mipLevels*/ 1, cmd,
        VK_FORMAT_R8G8B8A8_UNORM,
        VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
    checkerImage.Update(loadedTex, cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

    window.FinishInit();
    std::println("GPU init submit + wait completed without validation errors.");
    std::println("BLAS device address: 0x{:x}", cubeMesh.blasAddr);

    // Cleanup happens via Window/Device dtors when they go out of scope.
    return 0;
}
