/*
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
*/

// Image2D<T> — portable 2D image type whose API surface is intentionally
// backend-specific via #ifdef. On Vulkan it aliases the existing
// ImageVulkan<T> (full VkFormat / usage / layout control). On WebGPU it's
// a thin handle around an rgba8unorm GPUTexture; sizes are u16 and the
// only update path is from a CompressedTextureAsset.
//
// The "no shared no-op signatures" principle is deliberate: callers do
// the same #ifdef the library does, and write the backend-specific
// invocation. The unified type name Image2D<T> is the only thing
// portable between the two — that's the whole point.

export module Crafter.Graphics:Image2D;

#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import :ImageVulkan;

export namespace Crafter {
    // Vulkan target: Image2D is just the existing ImageVulkan. New name,
    // same shape — keeps existing ImageVulkan callers (e.g. examples/
    // Decompression) working without a churn-rename.
    template <typename PixelType>
    using Image2D = ImageVulkan<PixelType>;
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM

#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import Crafter.Asset;
import :DescriptorHeapWebGPU;
import :WebGPU;
import :WebGPUComputeShader;   // WebGPUTexelFormat

export namespace Crafter {
    // GPU-produced 2D texture with STORAGE_BINDING + TEXTURE_BINDING usage
    // and a runtime-chosen format. The HDR post-process building block: an
    // rgba16float target a compute pass can write through a StorageTexture
    // binding and a later pass can sample through a SampledTexture binding.
    // No CompressedTextureAsset / CPU upload — content comes from the GPU.
    // Distinct from Image2D<T> (rgba8unorm, sampled-only, asset-upload).
    class StorageImage2D {
    public:
        WebGPUTextureRef  handle = 0;
        std::uint16_t     width  = 0;
        std::uint16_t     height = 0;
        WebGPUTexelFormat format = WebGPUTexelFormat::RGBA16Float;

        void Create(std::uint16_t w, std::uint16_t h,
                    WebGPUTexelFormat fmt = WebGPUTexelFormat::RGBA16Float) {
            width  = w;
            height = h;
            format = fmt;
            handle = WebGPU::wgpuCreateStorageImage2D(
                w, h, static_cast<std::int32_t>(fmt));
        }

        // Register in a heap image slot so a UI custom shader can bind it
        // via UICustomBinding (SampledTexture to read, StorageTexture to
        // write). PlainComputeShader / RTPass take the raw `handle` directly.
        ImageSlot AllocateSlot(DescriptorHeapWebGPU& heap) {
            DescriptorRange r = heap.AllocateImageSlots(1);
            heap.imageTable[r.firstElement] = handle;
            return ImageSlot(&heap, r.firstElement);
        }

        void Destroy() {
            if (handle != 0) {
                WebGPU::wgpuDestroyTexture(handle);
                handle = 0;
            }
        }
    };

    template <typename PixelType>
    class Image2D {
    public:
        WebGPUTextureRef handle = 0;
        std::uint16_t width  = 0;
        std::uint16_t height = 0;

        void Create(std::uint16_t w, std::uint16_t h) {
            width  = w;
            height = h;
            handle = WebGPU::wgpuCreateImage2D(w, h);
        }

        // CPU-decompress the .ctex blob (no GPU decompression on WebGPU)
        // and upload via wgpuWriteImage2D. The intermediate `pixels` vector
        // lives only for the duration of this call — the underlying
        // queue.writeTexture in JS makes its own copy.
        void Update(const CompressedTextureAsset& asset) {
            if (asset.pixelStride != sizeof(PixelType)) {
                std::println(std::cerr,
                    "Image2D::Update: pixel stride mismatch (got {}, expected {})",
                    asset.pixelStride, sizeof(PixelType));
                std::abort();
            }
            std::vector<PixelType> pixels(
                static_cast<std::size_t>(asset.sizeX) * asset.sizeY);
            std::array<std::span<std::byte>, 1> outputs = {
                std::as_writable_bytes(std::span(pixels)),
            };
            Compression::DecompressCPU(asset.blob, outputs);
            WebGPU::wgpuWriteImage2D(
                handle,
                pixels.data(),
                static_cast<std::int32_t>(pixels.size() * sizeof(PixelType)),
                asset.sizeX, asset.sizeY);
        }

        // Register the texture in a descriptor heap slot so a custom RT
        // pipeline can bind it via UICustomBinding::SampledTexture.
        ImageSlot AllocateSlot(DescriptorHeapWebGPU& heap) {
            DescriptorRange r = heap.AllocateImageSlots(1);
            heap.imageTable[r.firstElement] = handle;
            return ImageSlot(&heap, r.firstElement);
        }

        void Destroy() {
            if (handle != 0) {
                WebGPU::wgpuDestroyTexture(handle);
                handle = 0;
            }
        }
    };

    // 2D texture array — `layers` × (w × h) rgba8unorm. Each layer is
    // populated independently from a CompressedTextureAsset whose dims
    // must match the array's (w × h). Layer 0 is sampled at array
    // index 0 in WGSL; bind through UICustomBindingKind::SampledTextureArray.
    template <typename PixelType>
    class Image2DArray {
    public:
        WebGPUTextureRef handle = 0;
        std::uint16_t width  = 0;
        std::uint16_t height = 0;
        std::uint16_t layers = 0;
        std::uint8_t  mipLevels = 1;

        // Create an array with `layerCount` × (w × h) layers, each carrying
        // `mipLevels` mip levels. Pass mipLevels=1 (default) for a single
        // base level — matching the original no-mip behaviour. Caller is
        // responsible for uploading each level via UpdateLayer (which
        // handles CPU mip-chain generation when mipLevels > 1).
        void Create(std::uint16_t w, std::uint16_t h, std::uint16_t layerCount,
                    std::uint8_t mipLevelCount = 1) {
            width     = w;
            height    = h;
            layers    = layerCount;
            mipLevels = mipLevelCount;
            handle = WebGPU::wgpuCreateImage2DArray(w, h, layerCount, mipLevelCount);
        }

        // Decompress `tex`, generate a CPU box-filter mip chain (if
        // mipLevels > 1), and upload each level into `layer`. The asset's
        // base-level dims must match the array's (w × h) — resize
        // beforehand on the host with TextureAsset<RGBA8>::Resize() if
        // they don't. Pixel data is treated as raw bytes per channel for
        // the box filter — for non-color data (normal maps) this gives
        // approximate but adequate results; for sRGB-encoded color data
        // it's also approximate but visually fine for game textures.
        void UpdateLayer(std::uint16_t layer, const CompressedTextureAsset& tex) {
            if (tex.pixelStride != sizeof(PixelType)) {
                std::println(std::cerr,
                    "Image2DArray::UpdateLayer: pixel stride mismatch (got {}, expected {})",
                    tex.pixelStride, sizeof(PixelType));
                std::abort();
            }
            if (tex.sizeX != width || tex.sizeY != height) {
                std::println(std::cerr,
                    "Image2DArray::UpdateLayer: layer {} dims {}x{} don't match array dims {}x{}",
                    layer, tex.sizeX, tex.sizeY, width, height);
                std::abort();
            }
            std::vector<PixelType> pixels(static_cast<std::size_t>(width) * height);
            std::array<std::span<std::byte>, 1> outputs = {
                std::as_writable_bytes(std::span(pixels)),
            };
            Compression::DecompressCPU(tex.blob, outputs);

            // Upload level 0.
            WebGPU::wgpuWriteImage2DLayer(
                handle, layer, /*level*/ 0,
                pixels.data(),
                static_cast<std::int32_t>(pixels.size() * sizeof(PixelType)),
                width, height);

            // Generate + upload subsequent mip levels via a 2x2 box filter
            // on the previous level's bytes. Each channel is averaged
            // independently across 4 source texels.
            std::uint16_t srcW = width;
            std::uint16_t srcH = height;
            std::vector<PixelType> prev = std::move(pixels);
            for (std::uint8_t lvl = 1; lvl < mipLevels; ++lvl) {
                std::uint16_t dstW = std::max<std::uint16_t>(1, srcW >> 1);
                std::uint16_t dstH = std::max<std::uint16_t>(1, srcH >> 1);
                std::vector<PixelType> next(static_cast<std::size_t>(dstW) * dstH);
                constexpr std::size_t kChannels = sizeof(PixelType);
                auto srcBytes = reinterpret_cast<const std::uint8_t*>(prev.data());
                auto dstBytes = reinterpret_cast<std::uint8_t*>(next.data());
                for (std::uint16_t y = 0; y < dstH; ++y) {
                    std::uint16_t sy0 = static_cast<std::uint16_t>(y * 2);
                    std::uint16_t sy1 = static_cast<std::uint16_t>(std::min<std::int32_t>(sy0 + 1, srcH - 1));
                    for (std::uint16_t x = 0; x < dstW; ++x) {
                        std::uint16_t sx0 = static_cast<std::uint16_t>(x * 2);
                        std::uint16_t sx1 = static_cast<std::uint16_t>(std::min<std::int32_t>(sx0 + 1, srcW - 1));
                        std::size_t a = (static_cast<std::size_t>(sy0) * srcW + sx0) * kChannels;
                        std::size_t b = (static_cast<std::size_t>(sy0) * srcW + sx1) * kChannels;
                        std::size_t c = (static_cast<std::size_t>(sy1) * srcW + sx0) * kChannels;
                        std::size_t d = (static_cast<std::size_t>(sy1) * srcW + sx1) * kChannels;
                        std::size_t out = (static_cast<std::size_t>(y) * dstW + x) * kChannels;
                        for (std::size_t ch = 0; ch < kChannels; ++ch) {
                            std::uint32_t sum = static_cast<std::uint32_t>(srcBytes[a + ch])
                                              + static_cast<std::uint32_t>(srcBytes[b + ch])
                                              + static_cast<std::uint32_t>(srcBytes[c + ch])
                                              + static_cast<std::uint32_t>(srcBytes[d + ch]);
                            dstBytes[out + ch] = static_cast<std::uint8_t>((sum + 2u) >> 2);
                        }
                    }
                }
                WebGPU::wgpuWriteImage2DLayer(
                    handle, layer, /*level*/ lvl,
                    next.data(),
                    static_cast<std::int32_t>(next.size() * sizeof(PixelType)),
                    dstW, dstH);
                prev = std::move(next);
                srcW = dstW;
                srcH = dstH;
            }
        }

        ImageSlot AllocateSlot(DescriptorHeapWebGPU& heap) {
            DescriptorRange r = heap.AllocateImageSlots(1);
            heap.imageTable[r.firstElement] = handle;
            return ImageSlot(&heap, r.firstElement);
        }

        void Destroy() {
            if (handle != 0) {
                WebGPU::wgpuDestroyTexture(handle);
                handle = 0;
            }
        }
    };
}
#endif // CRAFTER_GRAPHICS_WINDOW_DOM
