/*
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
*/
module;
#include "vulkan/vulkan.h"
module Crafter.Graphics:UI_impl;
import :UI;
import :ComputeShader;
import :Device;
import :Window;
import :DescriptorHeapVulkan;
import :ImageVulkan;
import :VulkanBuffer;
import :FontAtlas;
import :Font;
import :GraphicsTypes;
import std;

using namespace Crafter;

// ─── Initialize ─────────────────────────────────────────────────────────

void UIRenderer::Initialize(Window& window, GraphicsDescriptorHeap& heap, GraphicsCommandBuffer initCmd,
                            std::filesystem::path quadsSpv,
                            std::filesystem::path circlesSpv,
                            std::filesystem::path imagesSpv,
                            std::filesystem::path textSpv,
                            std::filesystem::path fusedSpv) {
    window_ = &window;
    heap_   = &heap;

    // Load the four standard pipelines plus the fused uber-kernel (issue #47).
    drawQuads.Load(quadsSpv);
    drawCircles.Load(circlesSpv);
    drawImages.Load(imagesSpv);
    drawText.Load(textSpv);
    drawFused.Load(fusedSpv);

    // Allocate one image slot for the swapchain output. Each per-frame heap
    // copy will hold ITS frame's image at this slot.
    auto outRange = heap_->AllocateImageSlots(1);
    outImageSlot_ = ImageSlot{heap_, outRange.firstElement};

    WriteSwapchainDescriptors();

    // Optional font-atlas registration (user must have called atlas->Initialize
    // already before reaching here, so atlas->image is live).
    if (fontAtlas != nullptr) {
        auto atlasImg = heap_->AllocateImageSlots(1);
        fontAtlasImageSlot_   = ImageSlot{heap_, atlasImg.firstElement};
        fontAtlasSamplerSlot_ = RegisterLinearClampSampler();
        WriteFontAtlasDescriptor();
    }

    // Flush the host-mapped descriptor heaps so the GPU sees what we wrote.
    for (auto& h : heap_->resourceHeap) h.FlushDevice();
    for (auto& h : heap_->samplerHeap)  h.FlushDevice();

    // Re-bind the swapchain image descriptors after every resize. Window
    // already drained the queue and rebuilt the imageViews[] before
    // firing the event, so vkWriteResourceDescriptorsEXT is safe here.
    resizeSub_.SetEvent(&window.onResize, [this]() {
        WriteSwapchainDescriptors();
        for (auto& h : heap_->resourceHeap) h.FlushDevice();
    });

    (void)initCmd; // reserved for future image-layout tweaks
}

// ─── per-frame Record ───────────────────────────────────────────────────

void UIRenderer::Record(GraphicsCommandBuffer cmd, std::uint32_t frameIdx, Window& window) {
    // Reset per-frame state.
    firstDispatchThisFrame_ = true;

    // If text is in use, flush any glyphs that user-side ShapeText calls
    // produced during a previous frame's onBuild. (Ensure() during the
    // current onBuild also marks the atlas dirty; that's flushed on the
    // NEXT Record. For v1 this is fine because the current frame's text
    // dispatch reads whatever's already been uploaded, and brand-new glyphs
    // missing from the atlas this frame will simply render blank for one
    // frame and resolve next frame. To get them this frame, the user can
    // call atlas->Update(cmd) themselves at the top of onBuild.)
    if (fontAtlas != nullptr && fontAtlas->dirty) {
        fontAtlas->Update(cmd);
    }

    onBuild.Invoke({cmd, frameIdx});

    (void)window;
}

// ─── group-count helper ────────────────────────────────────────────────

namespace {
    // Number of 8-pixel tiles needed to cover `dim` pixels (rounded up).
    inline std::uint32_t TilesFor(std::uint32_t dim) {
        return (dim + 7u) / 8u;
    }
}

// ─── standard-shader convenience dispatches ─────────────────────────────
//
// All four standard shaders use the same pixel-tile dispatch model: one
// workgroup per 8×8 screen tile, each thread iterates every item in order
// inside the workgroup, accumulating into a local register. This guarantees
// "items in the buffer render in order" (later items overdraw earlier ones)
// without inter-workgroup races on imageLoad/imageStore — the bug that the
// per-item dispatch model had.

void UIRenderer::DispatchQuads(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
                               std::uint32_t itemCount,
                               std::array<float,4> clipRectPx) {
    if (itemCount == 0) return;
    struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
    Dispatch(cmd, drawQuads, &pc, sizeof(pc),
             TilesFor(window_->width), TilesFor(window_->height), 1u);
}

void UIRenderer::DispatchCircles(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
                                 std::uint32_t itemCount,
                                 std::array<float,4> clipRectPx) {
    if (itemCount == 0) return;
    struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
    Dispatch(cmd, drawCircles, &pc, sizeof(pc),
             TilesFor(window_->width), TilesFor(window_->height), 1u);
}

void UIRenderer::DispatchImages(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
                                std::uint32_t itemCount,
                                std::array<float,4> clipRectPx) {
    if (itemCount == 0) return;
    struct PC { UIDispatchHeader hdr; } pc { FillHeader(bufferSlot, itemCount, clipRectPx) };
    Dispatch(cmd, drawImages, &pc, sizeof(pc),
             TilesFor(window_->width), TilesFor(window_->height), 1u);
}

void UIRenderer::DispatchText(GraphicsCommandBuffer cmd, std::uint32_t bufferSlot,
                              std::uint32_t itemCount,
                              std::array<float,4> clipRectPx) {
    if (itemCount == 0) return;
    if (!fontAtlasImageSlot_) {
        throw std::runtime_error("UIRenderer::DispatchText: no FontAtlas registered (set fontAtlas before Initialize)");
    }
    // Flush any glyphs that ShapeText calls (during this onBuild) just
    // rasterised, so the dispatch below sees them.
    if (fontAtlas != nullptr && fontAtlas->dirty) {
        fontAtlas->Update(cmd);
    }
    struct PC {
        UIDispatchHeader hdr;
        std::uint32_t fontTextureSlot;
        std::uint32_t fontSamplerSlot;
        std::uint32_t _p0;
        std::uint32_t _p1;
    } pc {
        FillHeader(bufferSlot, itemCount, clipRectPx),
        fontAtlasImageSlot_,
        fontAtlasSamplerSlot_,
        0, 0
    };
    Dispatch(cmd, drawText, &pc, sizeof(pc),
             TilesFor(window_->width), TilesFor(window_->height), 1u);
}

// ─── fused dispatch (issue #47) ─────────────────────────────────────────
//
// One uber-kernel composites quads → circles → images → text into the output
// image with a single load and a single store. Routed through the generic
// Dispatch() so it still gets the standard write-after-write barrier when it
// is not the first dispatch of the frame, and a following dispatch barriers
// against it — the only barriers it removes are the INTRA-fuse ones a run of
// Dispatch* calls would have inserted between its categories.

void UIRenderer::DispatchFused(GraphicsCommandBuffer cmd,
                               const FusedBatch& quads,
                               const FusedBatch& circles,
                               const FusedBatch& images,
                               const FusedBatch& text) {
    // Nothing to draw → no dispatch (and, crucially, no barrier bump).
    if (quads.itemCount == 0 && circles.itemCount == 0 &&
        images.itemCount == 0 && text.itemCount == 0) {
        return;
    }

    if (text.itemCount != 0 && !fontAtlasImageSlot_) {
        throw std::runtime_error("UIRenderer::DispatchFused: text batch given but no FontAtlas registered (set fontAtlas before Initialize)");
    }
    // Flush any glyphs ShapeText rasterised during this onBuild, mirroring
    // DispatchText — only relevant when there is text to draw.
    if (text.itemCount != 0 && fontAtlas != nullptr && fontAtlas->dirty) {
        fontAtlas->Update(cmd);
    }

    UIFusedHeader pc{};
    pc.itemBuffers[0] = quads.bufferSlot;
    pc.itemBuffers[1] = circles.bufferSlot;
    pc.itemBuffers[2] = images.bufferSlot;
    pc.itemBuffers[3] = text.bufferSlot;
    pc.itemCounts[0]  = quads.itemCount;
    pc.itemCounts[1]  = circles.itemCount;
    pc.itemCounts[2]  = images.itemCount;
    pc.itemCounts[3]  = text.itemCount;
    pc.outImage       = outImageSlot_;
    pc.fontTexture    = fontAtlasImageSlot_;
    pc.fontSampler    = fontAtlasSamplerSlot_;
    // Per-category clip-active bits, reusing the same surface-coverage test the
    // standard path uses (ClipFlags). A category whose clip already covers the
    // surface clears its bit so the kernel skips that category's per-pixel clip
    // compares (the common full-surface case) — pixel-identical either way.
    auto clipBit = [&](const FusedBatch& b, std::uint32_t bit) -> std::uint32_t {
        return (ClipFlags(b.clipRectPx, window_->width, window_->height, 0) & kUIFlagClip) ? bit : 0u;
    };
    pc.flags          = clipBit(quads,   0x1u) | clipBit(circles, 0x2u)
                      | clipBit(images,  0x4u) | clipBit(text,    0x8u);
    pc.surfaceWidth   = window_->width;
    pc.surfaceHeight  = window_->height;
    pc.frameIdx       = window_->currentBuffer;
    pc._pad           = 0;
    std::copy(quads.clipRectPx.begin(),   quads.clipRectPx.end(),   pc.clipQuads);
    std::copy(circles.clipRectPx.begin(), circles.clipRectPx.end(), pc.clipCircles);
    std::copy(images.clipRectPx.begin(),  images.clipRectPx.end(),  pc.clipImages);
    std::copy(text.clipRectPx.begin(),    text.clipRectPx.end(),    pc.clipText);

    Dispatch(cmd, drawFused, &pc, sizeof(pc),
             TilesFor(window_->width), TilesFor(window_->height), 1u);
}

// ─── generic Dispatch (with barrier) ────────────────────────────────────

void UIRenderer::Dispatch(GraphicsCommandBuffer cmd, const GraphicsComputeShader& shader,
                          const void* push, std::uint32_t pushBytes,
                          std::uint32_t gx, std::uint32_t gy, std::uint32_t gz) {
    if (!firstDispatchThisFrame_) {
        // Every UI pass read-modify-writes the same swapchain storage image
        // (later passes overdraw earlier ones), so each dispatch must observe
        // the previous dispatch's writes. The only resource the hazard touches
        // is that one image, so scope the dependency to its subresource with a
        // VkImageMemoryBarrier rather than a queue-wide VkMemoryBarrier: same
        // correctness, but only this image's shader caches are flushed —
        // unrelated buffers/images are no longer invalidated. The image stays
        // in VK_IMAGE_LAYOUT_GENERAL (bound as a storage image), so this is a
        // pure memory dependency, no layout transition.
        //
        // The execution dependency is still COMPUTE->COMPUTE over the whole
        // queue, so the passes do NOT overlap — this is a narrower cache flush
        // only. Eliminating the barriers entirely requires fusing the passes
        // into one dispatch (tracked in #47); do not attempt that here.
        VkImageMemoryBarrier ib {
            .sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
            .srcAccessMask       = VK_ACCESS_SHADER_WRITE_BIT,
            .dstAccessMask       = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
            .oldLayout           = VK_IMAGE_LAYOUT_GENERAL,
            .newLayout           = VK_IMAGE_LAYOUT_GENERAL,
            .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
            .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
            .image               = window_->images[window_->currentBuffer],
            .subresourceRange    = {
                .aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT,
                .baseMipLevel   = 0,
                .levelCount     = 1,
                .baseArrayLayer = 0,
                .layerCount     = 1,
            },
        };
        vkCmdPipelineBarrier(cmd,
            VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
            VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
            0, 0, nullptr, 0, nullptr, 1, &ib);
    }
    firstDispatchThisFrame_ = false;
    shader.Dispatch(cmd, push, pushBytes, gx, gy, gz);
}

// ─── descriptor writes ─────────────────────────────────────────────────

void UIRenderer::WriteSwapchainDescriptors() {
    // Each per-frame heap holds ITS swapchain image at outImageSlot_.
    std::array<VkImageDescriptorInfoEXT, Window::numFrames>    infos{};
    std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
    std::array<VkHostAddressRangeEXT, Window::numFrames>       destinations{};

    for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
        infos[f] = {
            .sType  = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
            .pView  = &window_->imageViews[f],
            .layout = VK_IMAGE_LAYOUT_GENERAL,
        };
        resources[f] = {
            .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
            .type  = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE,
            .data  = { .pImage = &infos[f] },
        };
        destinations[f] = {
            .address = heap_->resourceHeap[f].value
                     + heap_->ImageByteOffset(outImageSlot_),
            .size    = Device::descriptorHeapProperties.imageDescriptorSize,
        };
    }
    Device::vkWriteResourceDescriptorsEXT(
        Device::device, Window::numFrames, resources.data(), destinations.data()
    );
}

void UIRenderer::WriteFontAtlasDescriptor() {
    atlasViewCreateInfo_ = {
        .sType    = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
        .image    = fontAtlas->image.image,
        .viewType = VK_IMAGE_VIEW_TYPE_2D,
        .format   = VK_FORMAT_R8_UNORM,
        .components = {
            VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
            VK_COMPONENT_SWIZZLE_IDENTITY, VK_COMPONENT_SWIZZLE_IDENTITY,
        },
        .subresourceRange = {
            .aspectMask     = VK_IMAGE_ASPECT_COLOR_BIT,
            .baseMipLevel   = 0,
            .levelCount     = 1,
            .baseArrayLayer = 0,
            .layerCount     = 1,
        },
    };
    WriteSampledImageDescriptor(fontAtlasImageSlot_,
                                atlasViewCreateInfo_,
                                VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
}

void UIRenderer::WriteSampledImageDescriptor(std::uint16_t slot,
                                             const VkImageViewCreateInfo& viewInfo,
                                             VkImageLayout layout) {
    std::array<VkImageDescriptorInfoEXT, Window::numFrames>    infos{};
    std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
    std::array<VkHostAddressRangeEXT, Window::numFrames>       destinations{};

    for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
        infos[f] = {
            .sType  = VK_STRUCTURE_TYPE_IMAGE_DESCRIPTOR_INFO_EXT,
            .pView  = &viewInfo,
            .layout = layout,
        };
        resources[f] = {
            .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
            .type  = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
            .data  = { .pImage = &infos[f] },
        };
        destinations[f] = {
            .address = heap_->resourceHeap[f].value + heap_->ImageByteOffset(slot),
            .size    = Device::descriptorHeapProperties.imageDescriptorSize,
        };
    }
    Device::vkWriteResourceDescriptorsEXT(
        Device::device, Window::numFrames, resources.data(), destinations.data()
    );
    for (auto& h : heap_->resourceHeap) h.FlushDevice();
}

void UIRenderer::WriteBufferDescriptor(std::uint16_t slot, VkDeviceAddress address, std::uint32_t size) {
    std::array<VkDeviceAddressRangeEXT, Window::numFrames>     ranges{};
    std::array<VkResourceDescriptorInfoEXT, Window::numFrames> resources{};
    std::array<VkHostAddressRangeEXT, Window::numFrames>       destinations{};

    for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
        ranges[f] = { .address = address, .size = size };
        resources[f] = {
            .sType = VK_STRUCTURE_TYPE_RESOURCE_DESCRIPTOR_INFO_EXT,
            .type  = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER,
            .data  = { .pAddressRange = &ranges[f] },
        };
        destinations[f] = {
            .address = heap_->resourceHeap[f].value + heap_->BufferByteOffset(slot),
            .size    = Device::descriptorHeapProperties.bufferDescriptorSize,
        };
    }
    Device::vkWriteResourceDescriptorsEXT(
        Device::device, Window::numFrames, resources.data(), destinations.data()
    );
    for (auto& h : heap_->resourceHeap) h.FlushDevice();
}

SamplerSlot UIRenderer::RegisterSampler(const VkSamplerCreateInfo& info) {
    auto range = heap_->AllocateSamplerSlots(1);
    std::array<VkSamplerCreateInfo, Window::numFrames>   infos{};
    std::array<VkHostAddressRangeEXT, Window::numFrames> destinations{};
    for (std::uint32_t f = 0; f < Window::numFrames; ++f) {
        infos[f] = info;
        destinations[f] = {
            .address = heap_->samplerHeap[f].value + heap_->SamplerByteOffset(range.firstElement),
            .size    = Device::descriptorHeapProperties.samplerDescriptorSize,
        };
    }
    Device::vkWriteSamplerDescriptorsEXT(
        Device::device, Window::numFrames, infos.data(), destinations.data()
    );
    for (auto& h : heap_->samplerHeap) h.FlushDevice();
    return SamplerSlot{heap_, range.firstElement};
}

SamplerSlot UIRenderer::RegisterLinearClampSampler() {
    VkSamplerCreateInfo s {
        .sType         = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
        .magFilter     = VK_FILTER_LINEAR,
        .minFilter     = VK_FILTER_LINEAR,
        .mipmapMode    = VK_SAMPLER_MIPMAP_MODE_LINEAR,
        .addressModeU  = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
        .addressModeV  = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
        .addressModeW  = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
        .maxAnisotropy = 1.0f,
        .minLod        = 0.0f,
        .maxLod        = VK_LOD_CLAMP_NONE,
    };
    return RegisterSampler(s);
}

