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

// Issue #52: shaped-run cache for UIRenderer::ShapeText. The slow path
// (UTF-8 decode + per-glyph atlas lookup + layout) is memoized into an
// origin-relative run keyed by (Font*, pxSize, color, utf8); a hit only
// translates the cached run into the output buffer. This must be exactly
// byte-equivalent to the uncached path, since the glyph buffer is rewritten
// and re-flushed every frame regardless.
//
// What is asserted:
//   - A cache HIT produces byte-identical glyphs to the original (MISS) call.
//   - The HIT path does NOT touch the atlas (no Ensure → atlas stays clean),
//     while the MISS path rasterises new glyphs and dirties it.
//   - Translating the same string to a different (x, baselineY) shifts every
//     glyph by exactly that delta (cache stores origin-relative geometry).
//   - Center / Right alignment fold the advance-based shift into ShapeText
//     (Center = -advance/2, Right = -advance), matching the old two-pass code.
//   - outCapacity truncates the written count but still reports full advance.
//   - InvalidateFont drops the run; a re-shape afterwards is still correct.
//   - Distinct color / pxSize / font are distinct cache entries.
//
// Needs a headless Vulkan device (the FontAtlas image is a real GPU image),
// but no swapchain/window — ShapeText only touches the CPU-side atlas. The
// UIRenderer is used without Initialize(): ShapeText reads only fontAtlas.

#include "vulkan/vulkan.h"
#include <cstdlib>

import Crafter.Graphics;
import std;

using namespace Crafter;

namespace {

int failures = 0;

void Check(bool ok, std::string_view what) {
    std::println("{}  {}", ok ? "PASS" : "FAIL", what);
    if (!ok) ++failures;
}

VkCommandBuffer BeginCmd() {
    VkCommandBufferAllocateInfo allocInfo {
        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO,
        .commandPool = Device::commandPool,
        .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY,
        .commandBufferCount = 1,
    };
    VkCommandBuffer cmd = VK_NULL_HANDLE;
    Device::CheckVkResult(vkAllocateCommandBuffers(Device::device, &allocInfo, &cmd));
    VkCommandBufferBeginInfo beginInfo {
        .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
        .flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT,
    };
    Device::CheckVkResult(vkBeginCommandBuffer(cmd, &beginInfo));
    return cmd;
}

void SubmitWait(VkCommandBuffer cmd) {
    Device::CheckVkResult(vkEndCommandBuffer(cmd));
    VkSubmitInfo submitInfo {
        .sType = VK_STRUCTURE_TYPE_SUBMIT_INFO,
        .commandBufferCount = 1,
        .pCommandBuffers = &cmd,
    };
    Device::CheckVkResult(vkQueueSubmit(Device::queue, 1, &submitInfo, VK_NULL_HANDLE));
    Device::CheckVkResult(vkQueueWaitIdle(Device::queue));
    vkFreeCommandBuffers(Device::device, Device::commandPool, 1, &cmd);
}

bool GlyphEq(const GlyphItem& a, const GlyphItem& b) {
    return a.x == b.x && a.y == b.y && a.w == b.w && a.h == b.h
        && a.u0 == b.u0 && a.v0 == b.v0 && a.u1 == b.u1 && a.v1 == b.v1
        && a.r == b.r && a.g == b.g && a.b == b.b && a.a == b.a;
}

bool RunsEqual(std::span<const GlyphItem> a, std::span<const GlyphItem> b) {
    if (a.size() != b.size()) return false;
    for (std::size_t i = 0; i < a.size(); ++i)
        if (!GlyphEq(a[i], b[i])) return false;
    return true;
}

std::filesystem::path FindFont() {
    // The runner's cwd is not guaranteed: cfg.files copies the font next to
    // the binary, but `crafter-build test` may also run from the project root.
    for (const char* cand : {
            "font.ttf",
            "tests/ShapeTextCache/font.ttf",
            "../../examples/HelloUI/font.ttf" }) {
        if (std::filesystem::exists(cand)) return cand;
    }
    return "font.ttf";
}

} // namespace

int main() {
    Device::Initialize();

    FontAtlas atlas;
    {
        VkCommandBuffer cmd = BeginCmd();
        atlas.Initialize(cmd);
        SubmitWait(cmd);
    }

    Font font(FindFont());

    UIRenderer ui;
    ui.fontAtlas = &atlas;   // ShapeText needs only the atlas — no Initialize.

    const std::array<float, 4> white{1, 1, 1, 1};
    constexpr float kSize = 18.0f;
    const std::string_view kText = "Hover me";

    std::array<GlyphItem, 64> bufMiss{};
    std::array<GlyphItem, 64> bufHit{};

    // ── 1. First call is a MISS: it rasterises glyphs → atlas goes dirty. ──
    atlas.dirty = false;   // pretend a prior Update() flushed the atlas clean
    float advMiss = 0.0f;
    std::uint32_t nMiss = ui.ShapeText(font, kSize, 100.0f, 200.0f, kText, white,
                                       bufMiss.data(), bufMiss.size(), &advMiss);
    Check(nMiss > 0, "MISS produced glyphs for a non-empty string");
    Check(advMiss > 0.0f, "MISS reported a positive advance");
    Check(atlas.dirty, "MISS rasterised new glyphs and dirtied the atlas");

    // ── 2. Second call is a HIT: byte-identical, and atlas stays clean. ────
    atlas.dirty = false;
    float advHit = 0.0f;
    std::uint32_t nHit = ui.ShapeText(font, kSize, 100.0f, 200.0f, kText, white,
                                      bufHit.data(), bufHit.size(), &advHit);
    Check(nHit == nMiss, "HIT wrote the same glyph count as MISS");
    Check(advHit == advMiss, "HIT reported the same advance as MISS");
    Check(RunsEqual({bufMiss.data(), nMiss}, {bufHit.data(), nHit}),
          "HIT is byte-identical to MISS");
    Check(!atlas.dirty, "HIT did not touch the atlas (no re-rasterise)");

    // ── 3. Translation: a different origin shifts every glyph by the delta. ─
    std::array<GlyphItem, 64> bufMoved{};
    const float dx = 37.0f, dy = -15.0f;
    std::uint32_t nMoved = ui.ShapeText(font, kSize, 100.0f + dx, 200.0f + dy,
                                        kText, white, bufMoved.data(),
                                        bufMoved.size(), nullptr);
    bool shifted = (nMoved == nHit);
    for (std::uint32_t i = 0; i < nMoved && shifted; ++i) {
        shifted = std::abs((bufMoved[i].x - bufHit[i].x) - dx) < 1e-3f
               && std::abs((bufMoved[i].y - bufHit[i].y) - dy) < 1e-3f
               && bufMoved[i].w == bufHit[i].w && bufMoved[i].h == bufHit[i].h;
    }
    Check(shifted, "translate-only move shifts every glyph by exactly (dx, dy)");

    // ── 4. Alignment folded into ShapeText (Center = -adv/2, Right = -adv). ─
    std::array<GlyphItem, 64> bufCenter{};
    std::array<GlyphItem, 64> bufRight{};
    float advCenter = 0.0f, advRight = 0.0f;
    std::uint32_t nCenter = ui.ShapeText(font, kSize, 100.0f, 200.0f, kText, white,
                                         bufCenter.data(), bufCenter.size(),
                                         &advCenter, TextAlign::Center);
    std::uint32_t nRight = ui.ShapeText(font, kSize, 100.0f, 200.0f, kText, white,
                                        bufRight.data(), bufRight.size(),
                                        &advRight, TextAlign::Right);
    bool centerOk = (nCenter == nHit) && (advCenter == advHit);
    for (std::uint32_t i = 0; i < nCenter && centerOk; ++i)
        centerOk = std::abs((bufCenter[i].x - bufHit[i].x) - (-advHit * 0.5f)) < 1e-3f;
    Check(centerOk, "Center alignment shifts the run left by advance/2");

    bool rightOk = (nRight == nHit) && (advRight == advHit);
    for (std::uint32_t i = 0; i < nRight && rightOk; ++i)
        rightOk = std::abs((bufRight[i].x - bufHit[i].x) - (-advHit)) < 1e-3f;
    Check(rightOk, "Right alignment shifts the run left by full advance");

    // ── 5. outCapacity truncates count but still reports full advance. ─────
    std::array<GlyphItem, 64> bufTrunc{};
    const std::uint32_t capLimit = (nHit > 1) ? (nHit - 1) : 0;
    float advTrunc = 0.0f;
    std::uint32_t nTrunc = ui.ShapeText(font, kSize, 100.0f, 200.0f, kText, white,
                                        bufTrunc.data(), capLimit, &advTrunc);
    Check(nTrunc == capLimit, "writing is capped at outCapacity");
    Check(advTrunc == advHit, "advance is full even when the buffer truncates");

    // ── 6. InvalidateFont drops the run; a re-shape is still correct. ──────
    ui.InvalidateFont(font);
    std::array<GlyphItem, 64> bufReshape{};
    float advReshape = 0.0f;
    std::uint32_t nReshape = ui.ShapeText(font, kSize, 100.0f, 200.0f, kText, white,
                                          bufReshape.data(), bufReshape.size(),
                                          &advReshape);
    Check(nReshape == nHit && advReshape == advHit
          && RunsEqual({bufReshape.data(), nReshape}, {bufHit.data(), nHit}),
          "re-shape after InvalidateFont matches the original output");

    // ── 7. Distinct color and pxSize are distinct entries (different runs). ─
    std::array<GlyphItem, 64> bufRed{};
    std::uint32_t nRed = ui.ShapeText(font, kSize, 100.0f, 200.0f, kText,
                                      {1, 0, 0, 1}, bufRed.data(), bufRed.size(),
                                      nullptr);
    bool colorDistinct = (nRed == nHit);
    for (std::uint32_t i = 0; i < nRed && colorDistinct; ++i)
        colorDistinct = bufRed[i].r == 1.0f && bufRed[i].g == 0.0f && bufRed[i].b == 0.0f;
    Check(colorDistinct, "a different color yields a correctly-colored run");

    std::array<GlyphItem, 64> bufBig{};
    float advBig = 0.0f;
    ui.ShapeText(font, kSize * 2.0f, 100.0f, 200.0f, kText, white,
                 bufBig.data(), bufBig.size(), &advBig);
    Check(advBig > advHit * 1.5f, "a larger pxSize produces a wider advance");

    if (failures == 0) std::println("\nAll ShapeText cache checks passed.");
    else               std::println("\n{} ShapeText cache check(s) FAILED.", failures);
    return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}
