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

// Regression test for issue #51: FontAtlas::Update used to re-upload the
// whole 1024×1024 atlas every time a single glyph was rasterized. It now
// accumulates a dirty bounding box (FontAtlas::DirtyRect) and copies only
// that sub-rect. The bookkeeping is pure CPU math, so this test drives it
// directly — no Vulkan device or WebGPU context needed:
//
//   - a fresh rect is empty; the first Add seeds exact bounds
//   - further Adds grow the half-open union, never shrink it
//   - degenerate glyphs (w<=0 / h<=0) leave the rect untouched
//   - UploadBox converts (min,max) → (origin, extent) and clamps to the
//     atlas, and reports emptiness so Update can early-out
//   - FontAtlas::MarkDirty keeps the public `dirty` flag in lockstep with
//     the rect, and Reset() clears both notions of dirtiness
//
// The actual GPU copy params (bufferOffset / bufferRowLength on Vulkan,
// dst origin / bytesPerRow on WebGPU) are exercised end-to-end by rendering
// text in the HelloUI example on both backends.

#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;
}

// Read UploadBox into locals and compare against an expected (x,y,w,h).
bool BoxIs(const FontAtlas::DirtyRect& r, int limit,
           std::uint32_t ex, std::uint32_t ey, std::uint32_t ew, std::uint32_t eh) {
    std::uint32_t x = 99999, y = 99999, w = 99999, h = 99999;
    if (!r.UploadBox(limit, x, y, w, h)) return false;
    return x == ex && y == ey && w == ew && h == eh;
}

} // namespace

int main() {
    // --- DirtyRect: empty / seed / grow ------------------------------------
    {
        FontAtlas::DirtyRect r;
        Check(r.Empty(), "fresh rect is empty");
        std::uint32_t x, y, w, h;
        Check(!r.UploadBox(1024, x, y, w, h), "UploadBox on empty rect returns false");

        r.Add(10, 20, 4, 6);
        Check(!r.Empty(), "rect is non-empty after first Add");
        Check(BoxIs(r, 1024, 10, 20, 4, 6), "first Add seeds exact (origin, extent)");

        // A box fully inside the current one must not change anything.
        r.Add(11, 21, 1, 1);
        Check(BoxIs(r, 1024, 10, 20, 4, 6), "contained Add does not shrink or move the box");

        // Grow up-left and down-right; result is the union.
        r.Add(5, 8, 2, 2);          // extends min corner to (5,8)
        r.Add(100, 200, 10, 10);    // extends max corner to (110,210)
        Check(BoxIs(r, 1024, 5, 8, 105, 202), "Adds grow to the half-open union");
    }

    // --- DirtyRect: degenerate glyphs are no-ops ---------------------------
    {
        FontAtlas::DirtyRect r;
        r.Add(3, 3, 0, 5);
        r.Add(3, 3, 5, 0);
        r.Add(3, 3, -1, -1);
        Check(r.Empty(), "zero/negative-extent Adds leave the rect empty");

        r.Add(3, 3, 5, 5);
        r.Add(50, 50, 0, 0);  // must not move the max corner out
        Check(BoxIs(r, 1024, 3, 3, 5, 5), "degenerate Add after a real one is ignored");
    }

    // --- DirtyRect: clamp to atlas extent ----------------------------------
    {
        FontAtlas::DirtyRect r;
        r.Add(-4, -4, 8, 8);          // straddles the top-left corner
        Check(BoxIs(r, 1024, 0, 0, 4, 4), "negative origin clamps to 0, extent trimmed");

        FontAtlas::DirtyRect r2;
        r2.Add(1020, 1020, 16, 16);   // straddles the bottom-right corner
        Check(BoxIs(r2, 1024, 1020, 1020, 4, 4), "over-extent box clamps to the atlas edge");
    }

    // --- FontAtlas::MarkDirty keeps `dirty` and the rect in sync -----------
    {
        FontAtlas atlas;
        Check(!atlas.dirty && atlas.dirtyRect.Empty(), "default FontAtlas is clean");

        atlas.MarkDirty(0, 0, FontAtlas::kAtlasSize, FontAtlas::kAtlasSize);
        Check(atlas.dirty, "MarkDirty sets the public dirty flag");
        Check(BoxIs(atlas.dirtyRect, FontAtlas::kAtlasSize,
                    0, 0, FontAtlas::kAtlasSize, FontAtlas::kAtlasSize),
              "full-atlas MarkDirty covers the whole extent (the Initialize clear)");

        atlas.MarkDirty(8, 8, 16, 16);  // contained — full extent already dirty
        Check(BoxIs(atlas.dirtyRect, FontAtlas::kAtlasSize,
                    0, 0, FontAtlas::kAtlasSize, FontAtlas::kAtlasSize),
              "subsequent contained MarkDirty keeps the full extent");

        // Simulate the reset Update performs after a successful upload.
        atlas.dirty = false;
        atlas.dirtyRect.Reset();
        Check(!atlas.dirty && atlas.dirtyRect.Empty(), "post-upload reset clears dirtiness");

        // After reset, a single glyph marks only its own rect.
        atlas.MarkDirty(64, 128, 12, 20);
        Check(atlas.dirty, "a glyph after reset re-arms dirty");
        Check(BoxIs(atlas.dirtyRect, FontAtlas::kAtlasSize, 64, 128, 12, 20),
              "post-reset MarkDirty tracks just the new glyph, not the old full extent");
    }

    if (failures != 0) {
        std::println("{} check(s) failed", failures);
        return EXIT_FAILURE;
    }
    std::println("all checks passed");
    return EXIT_SUCCESS;
}
