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

// Backend-agnostic UIRenderer methods. FillHeader and ShapeText do not
// touch any GPU API; they only read window dimensions and (for ShapeText)
// the CPU-side font atlas. Split out so both Vulkan and WebGPU impls
// share the same source.

module Crafter.Graphics:UI_shared_impl;
import :UI;
import :Window;
import :Font;
import :FontAtlas;
import :GraphicsTypes;
import std;

using namespace Crafter;

UIDispatchHeader UIRenderer::FillHeader(std::uint32_t itemBufferSlot,
                                        std::uint32_t itemCount,
                                        std::array<float,4> clipRectPx,
                                        std::uint32_t flags) const noexcept {
    UIDispatchHeader h{};
    h.outImage      = outImageSlot_;
    h.itemBuffer    = itemBufferSlot;
    h.surfaceWidth  = window_->width;
    h.surfaceHeight = window_->height;
    h.clipX = clipRectPx[0];
    h.clipY = clipRectPx[1];
    h.clipW = clipRectPx[2];
    h.clipH = clipRectPx[3];
    h.itemCount = itemCount;
#ifndef CRAFTER_GRAPHICS_WINDOW_DOM
    h.frameIdx  = window_->currentBuffer;
#else
    h.frameIdx  = 0;
#endif
    h.flags     = ClipFlags(clipRectPx, window_->width, window_->height, flags);
    h._pad      = 0;
    return h;
}

std::uint32_t UIRenderer::ClipFlags(std::array<float,4> clipRectPx,
                                    std::uint32_t surfaceWidth,
                                    std::uint32_t surfaceHeight,
                                    std::uint32_t flags) noexcept {
    // The reserved kUIFlagClip bit signals "clip rect is narrower than the
    // surface". When the rect already covers the whole surface (the common
    // {0,0,1e9,1e9} default), the four per-pixel clip compares in
    // uiResolveScreenPixel can never reject an in-surface pixel, so we leave
    // the bit clear and the shader skips them — pixel-identical output.
    const bool clipCoversSurface =
        clipRectPx[0] <= 0.0f && clipRectPx[1] <= 0.0f
        && clipRectPx[0] + clipRectPx[2] >= static_cast<float>(surfaceWidth)
        && clipRectPx[1] + clipRectPx[3] >= static_cast<float>(surfaceHeight);
    return clipCoversSurface ? (flags & ~kUIFlagClip) : (flags | kUIFlagClip);
}

std::uint32_t UIRenderer::ShapeText(Font& font, float pxSize,
                                    float x, float baselineY,
                                    std::string_view utf8,
                                    std::array<float,4> color,
                                    GlyphItem* out, std::uint32_t outCapacity,
                                    float* outAdvance,
                                    TextAlign align) {
    if (fontAtlas == nullptr) {
        std::println("UIRenderer::ShapeText: no FontAtlas (set fontAtlas before Initialize)");
        std::abort();
    }

    // Look up (or build) the origin-relative shaped run. It is stored with
    // the pen starting at x=0, baseline=0 so a cache hit only needs the
    // translate below. The whole string is shaped (no outCapacity limit) so
    // a later call with a larger buffer still gets the full run.
    ShapedRunKey key{&font, pxSize, color, std::string(utf8)};
    auto it = shapedRuns_.find(key);
    if (it == shapedRuns_.end()) {
        // Miss: shape from scratch. Ensure() each glyph so brand-new ones
        // rasterise into the atlas and mark it dirty for the next upload —
        // a hit skips this, which is safe because atlas entries are never
        // evicted once placed.
        const float scale = pxSize / FontAtlas::kBaseSize;
        ShapedRun run;
        float cursor = 0.0f;
        std::size_t i = 0;
        while (i < utf8.size()) {
            std::uint32_t cp = DecodeUtf8(utf8, i);
            if (cp == 0) break;
            if (cp == '\n') { continue; }

            const Glyph* g = fontAtlas->EnsureAndGet(font, cp);
            if (g == nullptr) continue;

            if (g->w > 0 && g->h > 0) {
                GlyphItem gi;
                gi.x  = cursor + g->xoff * scale;
                gi.y  = g->yoff * scale;
                gi.w  = g->w * scale;
                gi.h  = g->h * scale;
                gi.u0 = g->u0; gi.v0 = g->v0;
                gi.u1 = g->u1; gi.v1 = g->v1;
                gi.r = color[0]; gi.g = color[1]; gi.b = color[2]; gi.a = color[3];
                run.glyphs.push_back(gi);
            }
            cursor += g->advance * scale;
        }
        run.advance = cursor;

        if (shapedRuns_.size() >= kMaxShapedRuns) shapedRuns_.clear();
        it = shapedRuns_.emplace(std::move(key), std::move(run)).first;
    }

    const ShapedRun& run = it->second;
    if (outAdvance) *outAdvance = run.advance;

    // Fold the alignment shift into the translate so callers don't need a
    // second pass over the glyphs.
    const float alignShift = (align == TextAlign::Center) ? -run.advance * 0.5f
                           : (align == TextAlign::Right)  ? -run.advance
                           :                                 0.0f;
    const float tx = x + alignShift;

    std::uint32_t written = 0;
    for (const GlyphItem& src : run.glyphs) {
        if (written >= outCapacity) break;
        GlyphItem& gi = out[written++];
        gi = src;
        gi.x += tx;
        gi.y += baselineY;
    }
    return written;
}

void UIRenderer::InvalidateFont(const Font& font) noexcept {
    std::erase_if(shapedRuns_, [&font](const auto& kv) {
        return kv.first.font == &font;
    });
}
