// Sponza on Vulkan + WebGPU. Same example source, two backends — picked
// by CRAFTER_GRAPHICS_WINDOW_DOM. Both paths:
//   1. Load a Sponza .cmesh (positions + indices, optional per-vertex
//      data region) and a single albedo .ctex from disk. The source
//      assets are fetched once by project.cpp (Crafter.Build::GitFetch)
//      from https://github.com/jimmiebergmann/Sponza and compressed
//      into the bin dir at build time — they don't live in this repo.
//   2. Build BLAS + TLAS via the existing Mesh / RenderingElement3D
//      flow. The on-disk format is identical between backends; only
//      the decompression path differs (VK_EXT_memory_decompression
//      on Vulkan, CPU GDeflate on WebGPU).
//   3. Upload the albedo as Image2D<RGBA8>, register it in the
//      backend descriptor heap, and run the RT pipeline. Closest-hit
//      shaders sample the texture at the hit's barycentric coords —
//      proof-of-binding rather than UV-correct shading. Per-vertex
//      UV interpolation is follow-up work (the attribs heap is in
//      place on WebGPU; the Vulkan side needs a sibling data buffer
//      exposed off Mesh).
//
// Sponza model: CC BY 3.0 — Frank Meinl (Crytek), packaged by Jimmie
// Bergmann and Morgan McGuire. https://casual-effects.com/data

#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif

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

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

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

    void RequireAssets(const fs::path& mesh, const fs::path& tex) {
        const bool haveMesh = fs::exists(mesh);
        const bool haveTex  = fs::exists(tex);
        if (haveMesh && haveTex) return;
        std::println(std::cerr,
            "[Sponza] missing asset(s):\n"
            "  mesh:    {} {}\n"
            "  albedo:  {} {}\n"
            "The build should have populated these via cfg.assets +\n"
            "GitFetch (see examples/Sponza/project.cpp). If you ran\n"
            "the binary from outside its bin dir, cd into the bin dir\n"
            "first — asset paths are relative to cwd.",
            mesh.string(), haveMesh ? "OK" : "MISSING",
            tex.string(),  haveTex  ? "OK" : "MISSING");
        std::abort();
    }
}

#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
int main() {
    // Native Vulkan path is single-material for now (see file header) —
    // pick up just the first per-material output the build emits. The
    // WebGPU branch below uses every mesh + a texture array.
    const fs::path meshPath = "mesh_0.cmesh";
    const fs::path texPath  = "tex_0.ctex";
    RequireAssets(meshPath, texPath);

    CompressedMeshAsset    loadedMesh = LoadCompressedMesh(meshPath);
    CompressedTextureAsset loadedTex  = LoadCompressedTexture(texPath);
    std::println("[Sponza] loaded {} verts, {} idx, {}x{} albedo",
                 loadedMesh.vertexCount, loadedMesh.indexCount,
                 loadedTex.sizeX, loadedTex.sizeY);

    Device::Initialize();
    Window window(1280, 720, "Sponza");
    VkCommandBuffer cmd = window.StartInit();

    DescriptorHeapVulkan descriptorHeap;
    descriptorHeap.Initialize(/*images*/ 2, /*buffers*/ 1, /*samplers*/ 0);

    // Two specialization constants: the TLAS slot offset (shared with
    // VulkanTriangle pattern) and the albedo slot index for closesthit.
    VkSpecializationMapEntry raygenEntry = { .constantID = 0, .offset = 0, .size = sizeof(std::uint16_t) };
    VkSpecializationInfo raygenSpec = {
        .mapEntryCount = 1, .pMapEntries = &raygenEntry,
        .dataSize = sizeof(std::uint16_t), .pData = &descriptorHeap.bufferStartElement,
    };

    // Allocate the albedo slot first so its index is known when we
    // compile closesthit.spv.
    auto imgSlots    = descriptorHeap.AllocateImageSlots(2);
    auto bufSlots    = descriptorHeap.AllocateBufferSlots(1);
    std::uint16_t albedoHeapSlot = static_cast<std::uint16_t>(imgSlots.firstElement + 1);

    VkSpecializationMapEntry hitEntry = { .constantID = 0, .offset = 0, .size = sizeof(std::uint16_t) };
    VkSpecializationInfo hitSpec = {
        .mapEntryCount = 1, .pMapEntries = &hitEntry,
        .dataSize = sizeof(std::uint16_t), .pData = &albedoHeapSlot,
    };

    std::array<VulkanShader, 3> shaders {{
        { "raygen.spv",     "main", VK_SHADER_STAGE_RAYGEN_BIT_KHR,      &raygenSpec },
        { "miss.spv",       "main", VK_SHADER_STAGE_MISS_BIT_KHR,        nullptr     },
        { "closesthit.spv", "main", VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR, &hitSpec    },
    }};
    ShaderBindingTableVulkan shaderTable;
    shaderTable.Init(shaders);

    std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> raygenGroups {{ {
        .sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
        .type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
        .generalShader = 0, .closestHitShader = VK_SHADER_UNUSED_KHR,
        .anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
    } }};
    std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> missGroups {{ {
        .sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
        .type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR,
        .generalShader = 1, .closestHitShader = VK_SHADER_UNUSED_KHR,
        .anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
    } }};
    std::array<VkRayTracingShaderGroupCreateInfoKHR, 1> hitGroups {{ {
        .sType = VK_STRUCTURE_TYPE_RAY_TRACING_SHADER_GROUP_CREATE_INFO_KHR,
        .type = VK_RAY_TRACING_SHADER_GROUP_TYPE_TRIANGLES_HIT_GROUP_KHR,
        .generalShader = VK_SHADER_UNUSED_KHR, .closestHitShader = 2,
        .anyHitShader = VK_SHADER_UNUSED_KHR, .intersectionShader = VK_SHADER_UNUSED_KHR,
    } }};

    PipelineRTVulkan pipeline;
    pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, shaderTable);

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

    Image2D<RGBA8> albedo;
    albedo.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);
    albedo.Update(loadedTex, cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
    SamplerVulkan<RGBA8> sampler;

    static RenderingElement3D renderer;
    renderer.instance = {
        .transform                              = {},
        .instanceCustomIndex                    = 0,
        .mask                                   = 0xFF,
        .instanceShaderBindingTableRecordOffset = 0,
        .flags                                  = VK_GEOMETRY_INSTANCE_FORCE_OPAQUE_BIT_KHR,
        .accelerationStructureReference         = sponzaMesh.blasAddr,
    };
    MatrixRowMajor<float, 4, 3, 1>::Identity()
        .Store(reinterpret_cast<float*>(renderer.instance.transform.matrix));
    RenderingElement3D::elements.emplace_back(&renderer);
    RenderingElement3D::BuildTLAS(cmd, 0);
    RenderingElement3D::BuildTLAS(cmd, 1);
    RenderingElement3D::BuildTLAS(cmd, 2);

    window.FinishInit();

    // Write descriptors: TLAS at bufSlots[0], output image at imgSlots[0],
    // albedo (combined image+sampler) at imgSlots[1]. Per-frame replicated.
    VkDeviceAddressRangeKHR tlasRanges[Window::numFrames];
    VkImageDescriptorInfoEXT outImgInfos[Window::numFrames];
    VkDescriptorImageInfo albedoInfo {
        .sampler = sampler.textureSampler,
        .imageView = albedo.imageView,
        .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
    };
    for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
        tlasRanges[f] = { .address = RenderingElement3D::tlases[f].address };
        outImgInfos[f] = {
            .sType = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
            .pView = &window.imageViews[f],
            .layout = VK_IMAGE_LAYOUT_GENERAL,
        };
    }

    std::vector<VkResourceDescriptorInfoEXT> resources;
    std::vector<VkHostAddressRangeEXT>       destinations;
    resources.reserve(Window::numFrames * 3);
    destinations.reserve(Window::numFrames * 3);
    for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
        resources.push_back({
            .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
            .type = VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHR,
            .data = { .pAddressRange = &tlasRanges[f] },
        });
        destinations.push_back({
            .address = descriptorHeap.resourceHeap[f].value
                     + descriptorHeap.BufferByteOffset(bufSlots.firstElement),
            .size = Device::descriptorHeapProperties.bufferDescriptorSize,
        });
        resources.push_back({
            .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
            .type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            .data = { .pImage = &outImgInfos[f] },
        });
        destinations.push_back({
            .address = descriptorHeap.resourceHeap[f].value
                     + descriptorHeap.ImageByteOffset(imgSlots.firstElement),
            .size = Device::descriptorHeapProperties.imageDescriptorSize,
        });
        resources.push_back({
            .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
            .type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
            .data = { .pCombinedImageSampler = &albedoInfo },
        });
        destinations.push_back({
            .address = descriptorHeap.resourceHeap[f].value
                     + descriptorHeap.ImageByteOffset(albedoHeapSlot),
            .size = Device::descriptorHeapProperties.imageDescriptorSize,
        });
    }
    Device::vkWriteResourceDescriptorsEXT(Device::device,
        static_cast<std::uint32_t>(resources.size()),
        resources.data(), destinations.data());
    for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
        descriptorHeap.resourceHeap[f].FlushDevice();
    }

    window.descriptorHeap = &descriptorHeap;
    RTPass rtPass(&pipeline);
    window.passes.push_back(&rtPass);

    window.Render();
    window.StartSync();
    return 0;
}
#else
int main() {
    // ── Read scene manifest (produced by project.cpp's ImportSponzaBundle).
    //
    //   line 1: albedoCount
    //   line 2: meshCount
    //   line 3..: per-mesh albedoIdx (-1 means "no albedo")
    const fs::path manifestPath = "scene.txt";
    if (!fs::exists(manifestPath)) {
        std::println(std::cerr,
            "[Sponza] missing scene.txt — the build should have produced "
            "it (see examples/Sponza/project.cpp). If you ran the binary "
            "from outside its bin dir, cd in first.");
        std::abort();
    }
    std::ifstream manifest(manifestPath);
    std::uint32_t albedoCount = 0, meshCount = 0;
    manifest >> albedoCount >> meshCount;
    std::vector<std::int32_t> meshAlbedo(meshCount);
    for (std::uint32_t i = 0; i < meshCount; ++i) manifest >> meshAlbedo[i];
    std::println("[Sponza] scene: {} albedos, {} meshes", albedoCount, meshCount);

    Device::Initialize();
    static Window window(1280, 720, "Sponza");
    auto cmd = window.StartInit();

    DescriptorHeapWebGPU heap;
    heap.Initialize(/*images*/ 2, /*buffers*/ 2, /*samplers*/ 2);

    std::array<WebGPUShader, 4> shaders {{
        WebGPUShader(fs::path("raygen.wgsl"),     "raygen_main",     WebGPURTStage::Raygen),
        WebGPUShader(fs::path("miss.wgsl"),       "miss_main",       WebGPURTStage::Miss),
        WebGPUShader(fs::path("closesthit.wgsl"), "closesthit_main", WebGPURTStage::ClosestHit),
        WebGPUShader(fs::path("resolve.wgsl"),    "resolve_main",    WebGPURTStage::Resolve),
    }};
    ShaderBindingTableWebGPU sbt;
    sbt.Init(shaders);

    std::array<RTShaderGroup, 1> raygenGroups {{
        { .type = RTShaderGroupType::General,           .generalShader = 0 },
    }};
    std::array<RTShaderGroup, 1> missGroups {{
        { .type = RTShaderGroupType::General,           .generalShader = 1 },
    }};
    std::array<RTShaderGroup, 1> hitGroups {{
        { .type = RTShaderGroupType::TrianglesHitGroup, .closestHitShader = 2 },
    }};

    // Three user bindings at @group(3) (the wavefront pipeline reserves
    // groups 0..2 for WfParams / data heaps / indirect args):
    //   binding 0 — albedo texture_2d_array (one layer per material)
    //   binding 1 — sampler (linear clamp)
    //   binding 2 — Camera storage buffer (host-driven, updated per frame)
    std::array<UICustomBinding, 3> bindings {{
        { .group = 3, .binding = 0, .kind = UICustomBindingKind::SampledTextureArray, .pushOffset = 0 },
        { .group = 3, .binding = 1, .kind = UICustomBindingKind::Sampler,             .pushOffset = 0 },
        { .group = 3, .binding = 2, .kind = UICustomBindingKind::Buffer,              .pushOffset = 0 },
    }};

    PipelineRTWebGPU pipeline;
    pipeline.Init(cmd, raygenGroups, missGroups, hitGroups, sbt, bindings);

    // ── Albedo texture array — one rgba8unorm layer per material. ──────
    //
    // Probe layer 0 for the canonical layer dimensions; project.cpp
    // already resized every albedo to the same square so any tex_N.ctex
    // would do, layer 0 is just the first one we have.
    Image2DArray<RGBA8> albedoArray;
    {
        CompressedTextureAsset probe = LoadCompressedTexture("tex_0.ctex");
        albedoArray.Create(probe.sizeX, probe.sizeY, static_cast<std::uint16_t>(albedoCount));
        albedoArray.UpdateLayer(0, probe);
        for (std::uint32_t i = 1; i < albedoCount; ++i) {
            CompressedTextureAsset tex = LoadCompressedTexture(std::format("tex_{}.ctex", i));
            albedoArray.UpdateLayer(static_cast<std::uint16_t>(i), tex);
        }
    }
    auto albedoArraySlot = albedoArray.AllocateSlot(heap);
    SamplerSlot samplerSlot = AllocateLinearClampSampler(heap);

    // Camera storage buffer — host writes (origin, right, up, forward,
    // aspect, tanHalf) every frame from the input-driven free camera
    // below. Layout matches the WGSL Camera struct in raygen.wgsl
    // (vec3-aligned, std430). 64 bytes total.
    struct CameraGPU {
        float origin[3];   float pad0;
        float right[3];    float tanHalf;
        float up[3];       float aspect;
        float forward[3];  float pad1;
    };
    static_assert(sizeof(CameraGPU) == 64);
    WebGPUBuffer<CameraGPU, true> cameraBuf;
    cameraBuf.Create(1);

    // Handle array fed to RTPass — order matches the bindings declaration.
    static std::array<std::uint32_t, 3> userHandles {
        heap.imageTable  [albedoArraySlot.firstElement],
        heap.samplerTable[samplerSlot.firstElement],
        cameraBuf.handle,
    };

    // ── Meshes + scene instances ───────────────────────────────────────
    //
    // One Mesh + one RenderingElement3D per material group from
    // scene.txt. Meshes whose albedoIdx is -1 (the .obj's `usemtl` named
    // something without a map_Kd in .mtl) get dropped — they're rare in
    // Sponza and we'd have nothing to sample for them anyway.
    //
    // Vector capacity is reserved up-front: RenderingElement3D::Add
    // takes a pointer that's stored in the static elements[] vector, so
    // any later vector reallocation would dangle those pointers.
    static std::vector<Mesh> meshes;
    static std::vector<RenderingElement3D> renderers;
    meshes.reserve(meshCount);
    renderers.reserve(meshCount);

    for (std::uint32_t i = 0; i < meshCount; ++i) {
        if (meshAlbedo[i] < 0) continue;
        CompressedMeshAsset loaded = LoadCompressedMesh(std::format("mesh_{}.cmesh", i));
        meshes.emplace_back();
        meshes.back().Build(loaded, cmd);

        renderers.emplace_back();
        RenderingElement3D& r = renderers.back();
        auto& tx = r.instance.transform.matrix;
        tx[0][0] = 1; tx[0][1] = 0; tx[0][2] = 0; tx[0][3] = 0;
        tx[1][0] = 0; tx[1][1] = 1; tx[1][2] = 0; tx[1][3] = 0;
        tx[2][0] = 0; tx[2][1] = 0; tx[2][2] = 1; tx[2][3] = 0;
        // 24-bit instanceCustomIndex carries the albedo array layer that
        // closesthit.wgsl reads as `hit.customIndex`.
        r.instance.instanceCustomIndex                    = static_cast<std::uint32_t>(meshAlbedo[i]);
        r.instance.mask                                   = 0xFF;
        r.instance.instanceShaderBindingTableRecordOffset = 0;
        r.instance.flags                                  = kRTGeometryInstanceForceOpaque;
        r.instance.accelerationStructureReference         = meshes.back().blasAddr;
        RenderingElement3D::Add(&r);
    }
    RenderingElement3D::BuildTLAS(cmd, 0);

    window.descriptorHeap = &heap;
    window.FinishInit();

    RTPass rtPass(&pipeline);
    rtPass.handlesPtr   = userHandles.data();
    rtPass.handlesCount = static_cast<std::uint32_t>(userHandles.size());
    rtPass.maxDepth     = 2;   // primary + shadow
    window.passes.push_back(&rtPass);

    // ── Free camera: WASD + mouse-delta look ───────────────────────────
    //
    // Initial pose puts the camera near one end of the atrium at eye
    // height, looking +X down the long axis (bbox: X[-1921..1800],
    // Y[-126..1429], Z[-1183..1105]). The user can fine-tune from there.
    struct CamState {
        // 3/4 view from a corner aimed at the atrium centre.
        Vector<float, 3, 4> position{ -1400.0f, 700.0f, -600.0f };
        float yaw   = 0.405f;   // radians, around world +Y
        float pitch = -0.317f;  // radians, +pitch looks up
    } cam;

    Input::Map inputMap;
    Input::Action& moveAct = inputMap.AddAction("Move", Input::ActionType::Vector2);
    Input::Action& lookAct = inputMap.AddAction("Look", Input::ActionType::Vector2);
    moveAct.bindings = {
        Input::WASDBind{
            Key(CrafterKeys::W), Key(CrafterKeys::S),
            Key(CrafterKeys::A), Key(CrafterKeys::D),
        },
    };
    lookAct.bindings = {
        Input::MouseDeltaBind{ 1.0f },
    };
    inputMap.Attach(window);

    constexpr float kMoveSpeed = 1200.0f;  // Sponza units / second (room is ~3700 wide)
    constexpr float kLookSens  = 0.05f;   // radians per mouse pixel
    constexpr float kDt        = 1.0f / 60.0f;

    EventListener<void> camTick(&window.onBeforeUpdate, [&]() {
        inputMap.Tick();

        cam.yaw   += lookAct.vector2.x * kLookSens;
        cam.pitch -= lookAct.vector2.y * kLookSens;
        // Keep pitch just shy of straight up/down so the basis vectors
        // don't collapse (cross(forward, world_up) would go zero).
        cam.pitch = std::clamp(cam.pitch, -1.55f, 1.55f);

        const float cp = std::cos(cam.pitch), sp = std::sin(cam.pitch);
        const float cy = std::cos(cam.yaw),   sy = std::sin(cam.yaw);
        Vector<float, 3, 4> forward { cp * cy, sp, cp * sy };
        Vector<float, 3, 4> worldUp { 0.0f, 1.0f, 0.0f };
        Vector<float, 3, 4> right { forward.y * worldUp.z - forward.z * worldUp.y,
                                    forward.z * worldUp.x - forward.x * worldUp.z,
                                    forward.x * worldUp.y - forward.y * worldUp.x };
        const float rLen = std::sqrt(right.x*right.x + right.y*right.y + right.z*right.z);
        right.x /= rLen; right.y /= rLen; right.z /= rLen;
        Vector<float, 3, 4> up { right.y * forward.z - right.z * forward.y,
                                 right.z * forward.x - right.x * forward.z,
                                 right.x * forward.y - right.y * forward.x };

        const float dx = moveAct.vector2.x * kMoveSpeed * kDt;
        const float dy = moveAct.vector2.y * kMoveSpeed * kDt;
        cam.position.x += right.x * dx + forward.x * dy;
        cam.position.y += right.y * dx + forward.y * dy;
        cam.position.z += right.z * dx + forward.z * dy;

        CameraGPU& g  = cameraBuf.value[0];
        g.origin[0]   = cam.position.x; g.origin[1]   = cam.position.y; g.origin[2]   = cam.position.z; g.pad0 = 0.0f;
        g.right[0]    = right.x;        g.right[1]    = right.y;        g.right[2]    = right.z;
        g.up[0]       = up.x;           g.up[1]       = up.y;           g.up[2]       = up.z;
        g.forward[0]  = forward.x;      g.forward[1]  = forward.y;      g.forward[2]  = forward.z;
        g.aspect      = float(window.width) / float(window.height);
        g.tanHalf     = std::tan(70.0f * 3.14159265f / 360.0f);
        g.pad1        = 0.0f;
        cameraBuf.FlushDevice();
    });

    window.Render();
    window.StartUpdate();
    window.StartSync();
    return 0;
}
#endif
