// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/VkRenderPassManager.h
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
//   https://www.gnu.org/licenses/gpl-3.0.txt
//   https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header

#pragma once

#include "SwapchainObject.h"
#include "VkClearManager.h"
#include "VkTextureManager.h"
#include "../VkIncludes.h"
#include "../VulkanRendererConfig.h"
#include "MG_State/GLState/FramebufferState/FramebufferObject.h"

#include <Includes.h>
#include <unordered_map>
#include <vk_mem_alloc.h>

namespace MobileGL::MG_Backend::DirectVulkan {
    enum class TrackedAttachmentTarget : Uint8 {
        Texture,
        Renderbuffer,
        SwapchainColor,
        SwapchainDepthStencil
    };

    struct PendingClearAttachmentInfo {
        // Index into the render pass attachment descriptions (VkRenderPassBeginInfo::pClearValues space).
        Uint32 attachmentIndex = 0;
        // Index into the subpass pColorAttachments (VkClearAttachment::colorAttachment space) — the GL
        // draw-buffer slot. Differs from attachmentIndex when earlier slots are GL_NONE/incomplete.
        // Only meaningful for color clears.
        Uint32 colorAttachmentSlot = 0;
        PendingClearKey key{};
        MG_State::GLState::RenderbufferObject* renderbuffer = nullptr;
        Bool hasInlinePayload = false;
        ClearAttachmentPayload inlinePayload{};
    };

    struct TrackedAttachmentLayoutInfo {
        TrackedAttachmentTarget target = TrackedAttachmentTarget::Texture;
        WeakPtr<MG_State::GLState::ITextureObject> texture;
        // Identity-compare shortcut for the per-draw "does the active pass use
        // this sampled texture" probe: comparing this against a LIVE texture's
        // address needs no weak_ptr::lock (two refcount atomics per probe).
        // May dangle once the texture dies - compare only, never dereference.
        MG_State::GLState::ITextureObject* textureRaw = nullptr;
        WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
        Uint32 textureMipLevel = 0;
        Uint32 swapchainImageIndex = 0;
        VkImageLayout finalLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    };

    struct DepthStencilAttachmentLoadInfo {
        VkAttachmentLoadOp depthLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
        VkAttachmentLoadOp stencilLoadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
        VkImageLayout initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    };

    DepthStencilAttachmentLoadInfo ResolveDepthStencilAttachmentLoadInfo(
        VkImageLayout trackedLayout, Bool clearDepth, Bool clearStencil);
    IntVec2 ResolveRenderPassFramebufferExtent(Bool isDefaultFbo, const TextureSize& attachmentExtent,
                                               VkExtent2D swapchainExtent);

    struct RenderPassEntry {
        static inline VkDevice s_device;
        static inline Vector<VkTextureManager::TextureResource*> s_textureResourcesScratch;
        Uint64 hash = 0;
        VkRenderPass renderPass = VK_NULL_HANDLE;
        VkFramebuffer framebuffer = VK_NULL_HANDLE;
        Uint64 compatibilityHash = 0;
        Vector<PendingClearAttachmentInfo> pendingClearAttachments;
        Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
        Uint32 attachmentCount = 0;
        Uint32 colorAttachmentCount = 0;
        Bool hasDepthStencilAttachment = false;
        VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
        IntVec2 extent = {0, 0};
        // VkFramebufferCreateInfo::layers of the entry's framebuffer (>1 for layered GL attachments).
        Uint32 layers = 1;
        // Frame counter value of the last GetOrCreateRenderPass hit; drives cache eviction.
        Uint64 lastUsedFrame = 0;

        RenderPassEntry() = default;
        RenderPassEntry(const RenderPassEntry&) = delete;
        RenderPassEntry(RenderPassEntry&& that) noexcept {
            std::swap(hash, that.hash);
            std::swap(renderPass, that.renderPass);
            std::swap(framebuffer, that.framebuffer);
            std::swap(compatibilityHash, that.compatibilityHash);
            std::swap(pendingClearAttachments, that.pendingClearAttachments);
            std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
            std::swap(attachmentCount, that.attachmentCount);
            std::swap(colorAttachmentCount, that.colorAttachmentCount);
            std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
            std::swap(sampleCount, that.sampleCount);
            std::swap(extent, that.extent);
            std::swap(layers, that.layers);
            std::swap(lastUsedFrame, that.lastUsedFrame);
        }
        // Move ASSIGNMENT, not just construction. The move constructor above and the
        // destructor below each independently suppress the implicit one, which left the
        // type move-constructible but not move-assignable - and therefore not swappable,
        // which std::swap(pair&, pair&) requires. That was invisible while UnorderedMap
        // only ever move-CONSTRUCTED an element into a fresh slot. ska::flat_hash_map
        // probes robin-hood: inserting swaps the entry being placed against the one
        // already sitting in the slot whenever it has travelled further from its desired
        // position, so the mapped type has to be swappable or the table fails to
        // instantiate at all.
        //
        // SWAP SEMANTICS, exactly like the move constructor: this does not release the
        // destination's handles, it parks them in `that`, which destroys them when it
        // dies. That is correct for the only caller - std::swap, whose temporary expires
        // immediately - and it is what keeps the three-move sequence from destroying a
        // live render pass. It is NOT correct for a hand-written `a = std::move(b)` where
        // `a` held live handles and `b` outlives the statement: those handles would then
        // survive until `b` dies. There is no such caller; add a destroy-then-steal
        // assignment before writing one.
        RenderPassEntry& operator=(RenderPassEntry&& that) noexcept {
            if (this != &that) {
                std::swap(hash, that.hash);
                std::swap(renderPass, that.renderPass);
                std::swap(framebuffer, that.framebuffer);
                std::swap(compatibilityHash, that.compatibilityHash);
                std::swap(pendingClearAttachments, that.pendingClearAttachments);
                std::swap(trackedAttachmentLayouts, that.trackedAttachmentLayouts);
                std::swap(attachmentCount, that.attachmentCount);
                std::swap(colorAttachmentCount, that.colorAttachmentCount);
                std::swap(hasDepthStencilAttachment, that.hasDepthStencilAttachment);
                std::swap(sampleCount, that.sampleCount);
                std::swap(extent, that.extent);
                std::swap(layers, that.layers);
                std::swap(lastUsedFrame, that.lastUsedFrame);
            }
            return *this;
        }
        RenderPassEntry(
            Uint64 hash,
            VkRenderPass renderpass,
            VkFramebuffer framebuffer,
            Uint64 compatibilityHash,
            const Vector<PendingClearAttachmentInfo>& pendingClearAttachments,
            const Vector<TrackedAttachmentLayoutInfo>& trackedAttachmentLayouts,
            Uint32 attachmentCount,
            Uint32 colorAttachmentCount,
            Bool hasDepthStencilAttachment,
            VkSampleCountFlagBits sampleCount,
            IntVec2 extent, Uint32 layers):
            hash(hash),
            renderPass(renderpass),
            framebuffer(framebuffer),
            compatibilityHash(compatibilityHash),
            pendingClearAttachments(Move(pendingClearAttachments)),
            trackedAttachmentLayouts(Move(trackedAttachmentLayouts)),
            attachmentCount(attachmentCount),
            colorAttachmentCount(colorAttachmentCount),
            hasDepthStencilAttachment(hasDepthStencilAttachment),
            sampleCount(sampleCount),
            extent(extent),
            layers(layers)
        {}

        ~RenderPassEntry() {
            if (renderPass != VK_NULL_HANDLE) {
                vkDestroyRenderPass(s_device, renderPass, nullptr);
            }
            if (framebuffer != VK_NULL_HANDLE) {
                vkDestroyFramebuffer(s_device, framebuffer, nullptr);
            }
        }

        Bool CompatibleWith(const RenderPassEntry& that) const {
            return this->compatibilityHash == that.compatibilityHash;
        }

        Bool CompatibleWith(Uint64 compatibilityHash) const {
            return this->compatibilityHash == compatibilityHash;
        }
    };

    struct ActiveRenderPassInfo {
        Uint64 hash = 0;
        Uint64 compatibilityHash = 0;
        Vector<TrackedAttachmentLayoutInfo> trackedAttachmentLayouts;
        IntVec2 extent = {0, 0};

        Bool CompatibleWith(const RenderPassEntry& that) const {
            return compatibilityHash == that.compatibilityHash;
        }

        Bool CompatibleWith(Uint64 thatCompatibilityHash) const {
            return compatibilityHash == thatCompatibilityHash;
        }
    };

    class VkRenderPassManager {
    public:
        using HashType = Uint64;

        // Notified once per OnPresent sweep with every aged-out entry's VkRenderPass
        // value: pipelines are hashed on the raw handle, and once destroyed the value
        // may be recycled for an incompatible pass, so dependent caches must purge
        // everything keyed on them before any new pass can be created (the sweep and
        // the notification run back-to-back with no creation in between; observers
        // compare the values, never dereference them). Batched so a mass-idle cohort
        // (shader-pack switch, dimension exit) costs the observer one pipeline-cache
        // scan, not one per dying pass. The wholesale paths
        // (Shutdown/RecreateSwapchain) do not notify - their callers already drop
        // every pipeline outright.
        class IEvictionObserver {
        public:
            virtual ~IEvictionObserver() = default;
            virtual void OnRenderPassesDestroyed(const Vector<VkRenderPass>& renderPasses) = 0;
        };

        VkRenderPassManager(VkDevice device,
            VkPhysicalDevice physicalDevice, VmaAllocator allocator, const VulkanRendererConfig& config,
            VkClearManager& clearManager, VkTextureManager& textureManager, SwapchainObject& swapchainObject);
        ~VkRenderPassManager();

        // Observer may be null (no notifications). Not owned.
        void SetEvictionObserver(IEvictionObserver* observer) { m_evictionObserver = observer; }

        Bool Initialize();
        void Shutdown();

        HashType ComputeHash(
            const MG_State::GLState::FramebufferObject& fbo,
            Uint32 swapchainImageIndex,
            Bool includePendingClear = true,
            Bool includeDefaultFboDepthStencil = true);
        // drawUsesDepthStencil: whether the operation about to run inside the pass
        // reads or writes the depth/stencil buffer (depth test or stencil test
        // enabled, or a depth/stencil clear). Only consulted for the DEFAULT
        // framebuffer: EGL undefines its ancillary buffers at every swap, so a
        // default-FBO pass whose draws provably never touch depth/stencil is
        // created WITHOUT the depth attachment - on a tiler that skips the whole
        // depth tile load AND store. The flavor only escalates: once a pass with
        // depth is active, later depth-less draws keep using it, and a depth-using
        // draw against a depth-less active pass resolves to a new (incompatible)
        // entry, which the caller's compatibility check turns into a pass split;
        // the new pass's depth loads DONT_CARE (content was undefined all along).
        //
        // Returns NULLPTR when this framebuffer cannot be represented as a Vulkan render pass at
        // all - a texture the texture manager declined to back (an unsupported format or sample
        // count), or an attachment view it cannot construct (a layer span the image has no room
        // for, a 3D image whose format was refused 2D-array compatibility). This used to be
        // unrepresentable: the function returned a reference, so the only thing the two fallible
        // calls it builds on could do was trip a MOBILEGL_ASSERT - which is compiled out of every
        // INFO build - and then dereference the null resource, or hand VK_NULL_HANDLE to
        // vkCreateFramebuffer. That took the whole process down (51 lost CTS records over 21
        // bodies, one runner restart each) where a declined draw is merely a wrong picture.
        //
        // EVERY caller must handle nullptr by dropping the operation, exactly as the draw path
        // already drops a draw whose sampler descriptor could not be resolved
        // (UniformManager::BindProgramUniformBuffers). The failure paths log MGLOG_E_ONCE
        // themselves, so a caller needs no message of its own.
        [[nodiscard]] RenderPassEntry* GetOrCreateRenderPass(const MG_State::GLState::FramebufferObject& fbo,
                                                             Uint32 swapchainImageIndex,
                                                             Bool drawUsesDepthStencil = true);
        void QueueRenderbufferClear(GLbitfield mask, const ClearFramebufferPayload& clearPayload,
                                    const MG_State::GLState::FramebufferObject& drawFbo);
        void QueueRenderbufferClear(const ClearAttachmentPayload& clearPayload,
                                    const MG_State::GLState::FramebufferAttachmentObject& attachment);
        void PopPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer);
        // Frame boundary hook: ages the render-pass cache and evicts long-unused
        // entries (their command buffers retired many frames ago).
        void OnPresent();
        static Bool BeginRenderPass(VkCommandBuffer commandBuffer, RenderPassEntry& renderPassEntry);
        static Bool EndRenderPass(VkCommandBuffer commandBuffer);
        static ActiveRenderPassInfo* GetActiveRenderPass();
    private:
        VkDevice m_device = VK_NULL_HANDLE;
        VkPhysicalDevice m_physicalDevice = VK_NULL_HANDLE;
        VmaAllocator m_allocator = nullptr;
        const VulkanRendererConfig& m_config;
        VkClearManager& m_clearManager;
        VkTextureManager& m_textureManager;
        SwapchainObject& m_swapchainObject;
        UnorderedMap<Uint64, RenderPassEntry> m_renderPasses;
        // Monotonic frame counter (bumped in OnPresent) for render-pass cache aging.
        Uint64 m_frameCounter = 0;
        IEvictionObserver* m_evictionObserver = nullptr;

        // Bumped whenever a renderbuffer VkImage is (re)created; together with the texture
        // manager's image epoch this invalidates the render-pass fast path on any attachment
        // image recreation.
        Uint64 m_renderbufferImageEpoch = 1;

    public:
        // Bumped whenever a renderbuffer backing is (re)created; consecutive-draw
        // snapshots include it so an attachment respecify forces a re-resolve.
        Uint64 GetRenderbufferImageEpoch() const { return m_renderbufferImageEpoch; }

    private:

        // Per-draw fast-path memo for GetOrCreateRenderPass (dirty-flag state tracking): when the
        // framebuffer state is provably unchanged since the last resolution, the active render pass
        // is reused WITHOUT recomputing the expensive per-draw hash. Invalidated by FBO switch /
        // version change, swapchain rotation, any attachment image recreation (the two epochs),
        // or a pending clear. Portable to Vulkan 1.1 (no dynamic_rendering / imageless FB needed).
        Bool m_rpFastValid = false;
        const MG_State::GLState::FramebufferObject* m_rpFastFbo = nullptr;
        // The FBO's never-reused lifetime id joins the raw pointer + Uint16 version:
        // a deleted FBO reallocated at the same address whose fresh setup performed
        // the same number of version bumps would otherwise compare equal (both count
        // from 0), serving the dead framebuffer's pass to the new object.
        Uint64 m_rpFastFboLifetimeId = 0;
        Uint16 m_rpFastFboVersion = 0;
        Uint32 m_rpFastSwapchainIndex = 0;
        Uint64 m_rpFastTexEpoch = 0;
        Uint64 m_rpFastRbEpoch = 0;
        Uint64 m_rpFastRenderPassHash = 0;
        // Whether the memoized entry carries a depth/stencil attachment; a
        // default-FBO resolution whose effective depth request differs must
        // miss the memo (the depth-less/depth-full flavors hash differently).
        Bool m_rpFastHadDepthStencil = false;

    public:
        struct RenderbufferResource {
            // deadSinceFrame sentinel: the owning weak reference has not been observed
            // expired. Dead resources age past every in-flight frame before Destroy
            // (see CollectRenderbufferGarbage); the GPU may still reference the image
            // for frames-in-flight frames after the GL object dies.
            static constexpr Uint64 kNeverObservedDead = UINT64_MAX;

            WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
            VkImage image = VK_NULL_HANDLE;
            VmaAllocation allocation = nullptr;
            VkImageView view = VK_NULL_HANDLE;
            // UNORM reinterpretation of an sRGB image, used as the attachment view while
            // GL_FRAMEBUFFER_SRGB is disabled (raw writes). Null for non-sRGB formats.
            VkImageView unormTwinView = VK_NULL_HANDLE;
            VkImageLayout layout = VK_IMAGE_LAYOUT_UNDEFINED;
            VkFormat format = VK_FORMAT_UNDEFINED;
            VkImageAspectFlags aspect = VK_IMAGE_ASPECT_NONE;
            VkExtent2D extent = {0, 0};
            VkSampleCountFlagBits sampleCount = VK_SAMPLE_COUNT_1_BIT;
            TextureInternalFormat internalFormat = TextureInternalFormat::Unknown;
            Int samples = 0;
            // m_frameCounter value at which the weak reference was first seen expired.
            Uint64 deadSinceFrame = kNeverObservedDead;

            void Destroy(VkDevice device, VmaAllocator allocator);
        };

        // Public so the renderer's blit/copy/readback bindings can source renderbuffer
        // attachments the same way texture attachments go through the texture manager.
        RenderbufferResource* GetOrCreateRenderbufferResource(
            const SharedPtr<MG_State::GLState::RenderbufferObject>& renderbuffer);
        Bool GetPendingRenderbufferClear(MG_State::GLState::RenderbufferObject* renderbuffer,
                                         ClearAttachmentPayload& outPayload) const;

    private:
        struct PendingRenderbufferClear {
            WeakPtr<MG_State::GLState::RenderbufferObject> renderbuffer;
            ClearAttachmentPayload payload{};
        };

        // A superseded renderbuffer backing (glRenderbufferStorage respecify) parked
        // until enough frame boundaries have passed that no in-flight command buffer
        // can still reference it; destroyed in OnPresent (see RetireAgeFrames).
        struct DeferredRenderbufferRelease {
            VkImage image = VK_NULL_HANDLE;
            VmaAllocation allocation = nullptr;
            VkImageView view = VK_NULL_HANDLE;
            VkImageView unormTwinView = VK_NULL_HANDLE;
            Uint64 deferredAtFrame = 0;
        };

        // Node-based std::unordered_map, deliberately NOT the open-addressing UnorderedMap:
        // callers cache a RenderbufferResource* - or a bare &resource->layout - and then make further
        // calls that touch this map. BlitFramebuffer is the one that bit: it resolves the source and
        // destination colour bindings (ResolveColorBlitBinding caches &rbResource->layout), then
        // materializes the source's pending clear, which looks that same resource up again. Growing
        // an open-addressed table relocates every element, so the cached pointer went on to name
        // freed storage still holding the pre-clear VK_IMAGE_LAYOUT_UNDEFINED; BlitFramebuffer bailed
        // out at "source image layout is undefined", silently dropping the blit -
        // renderbuffers_storage_multisample read back zero instead of the clear colour on exactly the
        // iterations that grew the table.
        //
        // Reordering the materialize ahead of the resolves - the fix ReadPixels got - does not cover
        // this: the destination resolve still runs after the source pointer is taken. The depth blit,
        // GetOrCreateRenderPass's depthRenderbufferResource and ReadDepthStencilPixels cache the same
        // kind of pointer, so the invariant belongs in the container rather than in a per-call-site
        // ordering rule. m_textureResources is node-based for the same reason.
        //
        // The case for keeping this node-based got STRONGER with ska::flat_hash_map, so do not read
        // the paragraph above as merely historical: ska erases by shifting the rest of the probe
        // cluster backwards into the hole, so erasing one renderbuffer relocates OTHER renderbuffers'
        // entries - a cached pointer can now be invalidated by a key it has nothing to do with, which
        // no call-site ordering rule can defend against. (What did change: ska's operator[] returns on
        // a hit before it runs its grow check, so a plain lookup of a PRESENT key no longer relocates.
        // That narrows the insert hazard; it does not touch the erase one.)
        std::unordered_map<MG_State::GLState::RenderbufferObject*, RenderbufferResource> m_renderbufferResources;
        UnorderedMap<MG_State::GLState::RenderbufferObject*, PendingRenderbufferClear> m_pendingRenderbufferClears;
        Vector<DeferredRenderbufferRelease> m_deferredRenderbufferReleases;
        // Supported sample counts per attachment format, so per-draw resource lookups
        // do not repeat vkGetPhysicalDeviceImageFormatProperties.
        UnorderedMap<VkFormat, VkSampleCountFlags> m_attachmentSampleCountsByFormat;

        Bool HasPendingRenderbufferClear(
            const MG_State::GLState::FramebufferAttachmentObject& attachment) const;
        void CollectRenderbufferGarbage();
        // Frame-boundary margin after which a resource last referenced by a retired
        // GL object (or superseded backing) is provably past every in-flight frame.
        Uint64 RetireAgeFrames() const;
        void DeferRenderbufferBackingRelease(RenderbufferResource& resource);
        void CollectDeferredRenderbufferReleases(Bool destroyAll);

        static inline XXH64_state_t* m_hashState = XXH64_createState();
        static inline ActiveRenderPassInfo s_activeRenderPass{};
        static inline Bool s_hasActiveRenderPass = false;
        static inline VkClearManager* s_clearManager = nullptr;
        static inline VkTextureManager* s_textureManager = nullptr;
        static inline SwapchainObject* s_swapchainObject = nullptr;
        static inline VkRenderPassManager* s_renderPassManager = nullptr;
    };
} // namespace MobileGL::MG_Backend::DirectVulkan
