/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
catcrafts.net
*/
module;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
#include "vulkan/vulkan.h"
#endif
#include "../lib/stb_truetype.h"
module Crafter.Graphics:FontAtlas_impl;
import :FontAtlas;
import :Font;
import :GraphicsTypes;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
import :ImageVulkan;
import :Device;
#else
import :WebGPU;
#endif
import std;

using namespace Crafter;

std::uint8_t* FontAtlas::PixelPtr() noexcept {
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
    return image.buffer.value;
#else
    return staging.data();
#endif
}

void FontAtlas::Initialize(GraphicsCommandBuffer cmd) {
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
    image.Create(
        kAtlasSize, kAtlasSize, /*mipLevels*/ 1, cmd,
        VK_FORMAT_R8_UNORM,
        VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
        VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        // The atlas is sampled by the UI text *compute* shader, not an RT
        // pipeline — scope the upload barriers to the real consumer.
        VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
    );
    std::memset(image.buffer.value, 0, kAtlasSize * kAtlasSize);
#else
    (void)cmd;
    staging.assign(kAtlasSize * kAtlasSize, 0);
    textureHandle = WebGPU::wgpuCreateAtlasTexture(kAtlasSize, kAtlasSize);
#endif
    // The freshly-zeroed atlas backs an image whose contents are undefined
    // (Vulkan) / unwritten (WebGPU). Mark the whole extent dirty so the
    // first Update clears it once; thereafter only glyph sub-rects flip
    // dirty, and every untouched texel stays the zero it was uploaded as —
    // so partial uploads keep the image byte-identical to the staging copy.
    MarkDirty(0, 0, kAtlasSize, kAtlasSize);
}

bool FontAtlas::ShelfPlace(int w, int h, int& outX, int& outY) {
    for (Shelf& s : shelves_) {
        if (h <= s.height && s.cursorX + w <= kAtlasSize) {
            outX = s.cursorX;
            outY = s.y;
            s.cursorX += w;
            return true;
        }
    }
    if (nextShelfY_ + h > kAtlasSize) return false;
    Shelf s{};
    s.y       = nextShelfY_;
    s.height  = h;
    s.cursorX = w;
    outX = 0;
    outY = s.y;
    shelves_.push_back(s);
    nextShelfY_ += h;
    return true;
}

bool FontAtlas::Ensure(Font& font, std::uint32_t codepoint) {
    return EnsureAndGet(font, codepoint) != nullptr;
}

const Glyph* FontAtlas::EnsureAndGet(Font& font, std::uint32_t codepoint) {
    Key key{&font, codepoint};
    if (auto it = cache_.find(key); it != cache_.end()) return &it->second;

    float fontScale = stbtt_ScaleForPixelHeight(&font.font, kBaseSize);

    int advanceUnits = 0, lsb = 0;
    stbtt_GetCodepointHMetrics(&font.font, static_cast<int>(codepoint), &advanceUnits, &lsb);

    int sw = 0, sh = 0, xoff = 0, yoff = 0;
    unsigned char* sdf = stbtt_GetCodepointSDF(
        &font.font, fontScale, static_cast<int>(codepoint),
        kPadding, static_cast<unsigned char>(kOnEdgeValue), kPixelDistScale,
        &sw, &sh, &xoff, &yoff
    );

    Glyph g{};
    g.advance = advanceUnits * fontScale;
    g.xoff = static_cast<float>(xoff);
    g.yoff = static_cast<float>(yoff);

    if (sdf && sw > 0 && sh > 0) {
        int px = 0, py = 0;
        if (!ShelfPlace(sw, sh, px, py)) {
            stbtt_FreeSDF(sdf, nullptr);
            return nullptr;
        }
        std::uint8_t* dst = PixelPtr();
        for (int row = 0; row < sh; ++row) {
            std::memcpy(
                dst + (py + row) * kAtlasSize + px,
                sdf + row * sw,
                static_cast<std::size_t>(sw)
            );
        }
        stbtt_FreeSDF(sdf, nullptr);

        g.w  = static_cast<float>(sw);
        g.h  = static_cast<float>(sh);
        g.u0 = static_cast<float>(px) / kAtlasSize;
        g.v0 = static_cast<float>(py) / kAtlasSize;
        g.u1 = static_cast<float>(px + sw) / kAtlasSize;
        g.v1 = static_cast<float>(py + sh) / kAtlasSize;
        MarkDirty(px, py, sw, sh);
    }

    return &cache_.emplace(key, g).first->second;
}

const Glyph* FontAtlas::Lookup(Font& font, std::uint32_t codepoint) const {
    auto it = cache_.find(Key{&font, codepoint});
    return it == cache_.end() ? nullptr : &it->second;
}

void FontAtlas::Update(GraphicsCommandBuffer cmd) {
    if (!dirty) return;

    // Clamp the accumulated box to the atlas and convert to (origin,
    // extent). Add() works in glyph coordinates that are always in-bounds,
    // but clamping keeps the copy extent provably valid. The `dirty` guard
    // above guarantees the rect is non-empty, so UploadBox always fills the
    // outputs.
    std::uint32_t x = 0, y = 0, w = 0, h = 0;
    dirtyRect.UploadBox(kAtlasSize, x, y, w, h);

#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
    image.UpdateRegion(cmd, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, x, y, w, h);
#else
    (void)cmd;
    // The staging buffer keeps the full atlas row stride, so srcBytesPerRow
    // stays kAtlasSize and (dstX, dstY) index the sub-rect's first texel.
    WebGPU::wgpuWriteAtlasRegion(
        textureHandle, staging.data(),
        kAtlasSize, kAtlasSize, kAtlasSize,
        static_cast<std::int32_t>(x), static_cast<std::int32_t>(y),
        static_cast<std::int32_t>(w), static_cast<std::int32_t>(h)
    );
#endif
    dirty = false;
    dirtyRect.Reset();
}
