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

// WebGPU buffer wrapper — DOM-mode parallel to VulkanBuffer<T, Mapped>.
// Holds a JS-side GPUBuffer handle + (when Mapped) a wasm-memory staging
// array. `.value` points to the staging memory; the user writes into it
// directly, and `.Flush(cmd)` copies to the GPU via queue.writeBuffer.

export module Crafter.Graphics:WebGPUBuffer;
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import :WebGPU;

export namespace Crafter {
    class WebGPUBufferBase {
    public:
        WebGPUBufferRef handle = 0;
        std::uint32_t size = 0;   // bytes
    };

    template<typename T>
    class WebGPUBufferMapped {
    public:
        T* value = nullptr;
    };
    class WebGPUBufferMappedEmpty {};

    template<typename T, bool Mapped>
    using WebGPUBufferMappedConditional =
        std::conditional_t<Mapped, WebGPUBufferMapped<T>, WebGPUBufferMappedEmpty>;

    template<typename T, bool Mapped>
    class WebGPUBuffer : public WebGPUBufferBase, public WebGPUBufferMappedConditional<T, Mapped> {
    public:
        WebGPUBuffer() = default;
        WebGPUBuffer(const WebGPUBuffer&) = delete;
        WebGPUBuffer& operator=(const WebGPUBuffer&) = delete;

        WebGPUBuffer(WebGPUBuffer&& other) noexcept {
            handle = other.handle;
            size   = other.size;
            other.handle = 0;
            if constexpr (Mapped) {
                this->value = other.value;
                other.value = nullptr;
            }
        }

        void Create(std::uint32_t count) {
            size = static_cast<std::uint32_t>(count * sizeof(T));
            handle = WebGPU::wgpuCreateBuffer(static_cast<std::int32_t>(size));
            if constexpr (Mapped) {
                this->value = new T[count]();
            }
        }

        void Clear() {
            if (handle != 0) {
                WebGPU::wgpuDestroyBuffer(handle);
                handle = 0;
            }
            if constexpr (Mapped) {
                if (this->value) { delete[] this->value; this->value = nullptr; }
            }
        }

        void Resize(std::uint32_t count) {
            if (handle != 0) Clear();
            Create(count);
        }

        void Flush(WebGPUCommandEncoderRef /*cmd*/) requires(Mapped) {
            WebGPU::wgpuWriteBuffer(handle, this->value, static_cast<std::int32_t>(size));
        }
        void FlushDevice() requires(Mapped) {
            WebGPU::wgpuWriteBuffer(handle, this->value, static_cast<std::int32_t>(size));
        }
        // Partial upload — write the bytes [srcByteOffset, srcByteOffset+byteCount)
        // of the host mirror to GPU offset `dstByteOffset`. BuildTLAS uses
        // this to leave the GPU-owned transform field of an RTInstance
        // intact (the physics-tlas-transform compute shader is its sole
        // writer) while still pushing the CPU-side metadata fields.
        void FlushDeviceRange(std::uint32_t dstByteOffset,
                              std::uint32_t srcByteOffset,
                              std::uint32_t byteCount) requires(Mapped) {
            const auto* base = reinterpret_cast<const char*>(this->value);
            WebGPU::wgpuWriteBufferRange(handle, dstByteOffset,
                                          base + srcByteOffset,
                                          static_cast<std::int32_t>(byteCount));
        }

        // Push one element's worth of bytes from the host mirror to GPU.
        // Use when a single SoA slot was mutated (body construction,
        // per-instance flag flip) and a full FlushDevice would clobber
        // the GPU-side updates the sim has applied to neighboring slots.
        void FlushDeviceSlot(std::uint32_t idx) requires(Mapped) {
            constexpr std::uint32_t kStride = sizeof(T);
            const std::uint32_t off = idx * kStride;
            FlushDeviceRange(off, off, kStride);
        }

        // Schedule a GPU→CPU readback of this buffer's entire contents.
        // Asynchronous; data isn't ready until a later PollReadback
        // returns true. Successive Enqueues without a Poll are dropped
        // — they're a no-op while the previous map is in flight.
        //
        // `resetBytes` ≥ 0 — if non-zero, the first `resetBytes` bytes
        // of THIS buffer are clearBuffer-cleared on the GPU command
        // encoder immediately after the copy, so the readback captures
        // the pre-clear bytes and the next frame's writers see zeros.
        // The reset is tied to a successful enqueue (skipped enqueue =
        // skipped reset), preserving accumulated state across missed
        // drains.
        void EnqueueReadback(std::uint32_t resetBytes = 0) {
            WebGPU::wgpuReadbackEnqueue(handle,
                                         static_cast<std::int32_t>(size),
                                         static_cast<std::int32_t>(resetBytes));
        }
        // Try to copy the readback bytes into this->value. Returns true
        // if the previous EnqueueReadback resolved and the data is now
        // mirrored into .value; false if the map is still pending.
        bool PollReadback() requires(Mapped) {
            return WebGPU::wgpuReadbackPoll(handle, this->value,
                                             static_cast<std::int32_t>(size)) != 0;
        }
        // Non-consuming readiness probe. Returns true if a subsequent
        // PollReadback would succeed without changing state otherwise.
        // Use to verify a sibling buffer is also ready before consuming.
        bool IsReadbackReady() const {
            return WebGPU::wgpuReadbackReady(handle) != 0;
        }

        ~WebGPUBuffer() { Clear(); }
    };
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
