/*
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 #56: InputField_HitTestCursor used to map a click
// x-coord to a cursor byte offset by calling Font::GetLineWidth on every
// prefix `value[0..i]` for i = 1..size — each call re-walks from byte 0, so it
// was O(n^2) glyph-metric lookups. Worse, it iterated raw byte boundaries, so
// on multibyte UTF-8 input it could return an offset that splits a codepoint
// (an invalid cursor position, since cursorPos is a byte offset edited via
// value.erase(cursorPos, 1)).
//
// The hit test now delegates to Font::NearestCursorByte: one left-to-right
// cumulative-advance pass (O(n) metrics) over codepoint boundaries, returning
// the byte offset of the boundary whose cumulative advance is nearest the
// target. This test pins:
//   - the result matches an independent brute-force "nearest codepoint
//     boundary" reference (built from per-prefix GetLineWidth) for many click
//     positions across ASCII and multibyte strings,
//   - clicks at/left of the text start return 0, clicks far past the end
//     return value.size() (the end-of-text caret),
//   - every returned offset lands on a codepoint boundary (never mid-sequence),
//   - clicking exactly on a glyph's left edge lands on that glyph's boundary.
//
// Pure CPU + a TrueType file — no Vulkan device or window needed. The font is
// copied next to the binary; the test also probes the project-root path.

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

std::filesystem::path FindFont() {
    for (const char* cand : {
            "font.ttf",
            "tests/InputFieldHitTest/font.ttf",
            "../../examples/HelloUI/font.ttf" }) {
        if (std::filesystem::exists(cand)) return cand;
    }
    return "font.ttf";
}

// All codepoint-boundary byte offsets of `s`, including 0 and s.size().
std::vector<std::size_t> Boundaries(std::string_view s) {
    std::vector<std::size_t> b{0};
    std::size_t i = 0;
    while (i < s.size()) {
        std::uint32_t cp = DecodeUtf8(s, i);   // advances i to the next boundary
        if (cp == 0) break;
        b.push_back(i);
    }
    return b;
}

// Independent reference: the codepoint boundary whose prefix advance is
// nearest `target`, ties resolved to the earlier boundary (matching the
// implementation's strict `d < bestDist`). Built with per-prefix GetLineWidth
// — the slow path the optimisation replaces, used here only as the oracle.
std::size_t NearestBoundaryRef(Font& font, std::string_view s, float size, float target) {
    if (target <= 0.0f) return 0;
    auto bounds = Boundaries(s);
    std::size_t best = 0;
    float bestDist = std::abs(target);   // boundary 0 has width 0
    for (std::size_t k = 1; k < bounds.size(); ++k) {
        float w = static_cast<float>(font.GetLineWidth(s.substr(0, bounds[k]), size));
        float d = std::abs(target - w);
        if (d < bestDist) {
            bestDist = d;
            best = bounds[k];
        }
        // No early break: the reference scans every boundary regardless, so a
        // mismatch would also catch any over-eager break in the implementation.
    }
    return best;
}

bool IsBoundary(std::string_view s, std::size_t off) {
    if (off > s.size()) return false;
    for (std::size_t b : Boundaries(s)) if (b == off) return true;
    return false;
}

const InputFieldColors kColors{
    {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1}, {0, 0, 0, 1},
    {1, 1, 1, 1}, {1, 1, 1, 1},
};

// Drive the public hit-test for a click at window-x `clickX`, given a field
// rect at `rectX`. paddingX is taken from kColors.
std::size_t Hit(const InputField& f, Font& font, float size, float rectX, float clickX) {
    Rect rect{rectX, 0.0f, 200.0f, 24.0f};
    return InputField_HitTestCursor(f, rect, clickX, font, size, kColors);
}

} // namespace

int main() {
    Font font(FindFont());

    constexpr float kSize = 18.0f;
    const float rectX = 30.0f;
    const float textStart = rectX + kColors.paddingX;   // x where the text begins

    const std::array<std::string_view, 4> samples = {
        "Hello, world",
        "1234567890",
        "iiiiWWWWmmmm",          // wildly varying advances
        "caf\xC3\xA9 na\xC3\xAFve \xE2\x9C\x93",   // "café naïve ✓" — 2- and 3-byte cps
    };

    // 1. Across each sample, sweep target px and compare to the oracle.
    bool sweepOk = true;
    bool allBoundaries = true;
    for (std::string_view s : samples) {
        InputField f;
        f.type  = InputFieldType::Text;
        f.value = std::string(s);
        float fullW = static_cast<float>(font.GetLineWidth(s, kSize));
        // Step finely enough to cross every glyph edge several times, and run
        // past the end of the text to exercise the end-of-text caret.
        for (float t = -10.0f; t <= fullW + 40.0f; t += 1.0f) {
            std::size_t got = Hit(f, font, kSize, rectX, textStart + t);
            std::size_t ref = NearestBoundaryRef(font, s, kSize, t);
            if (got != ref) {
                sweepOk = false;
                std::println("  mismatch on \"{}\": target={} got={} ref={}",
                             s, t, got, ref);
            }
            if (!IsBoundary(s, got)) allBoundaries = false;
        }
    }
    Check(sweepOk, "hit test matches the brute-force nearest-boundary oracle across a px sweep");
    Check(allBoundaries, "every returned offset lands on a UTF-8 codepoint boundary");

    // 2. Clicks at or left of the text start return 0.
    {
        InputField f; f.value = "Hello";
        Check(Hit(f, font, kSize, rectX, textStart) == 0,
              "click exactly at the text start returns offset 0");
        Check(Hit(f, font, kSize, rectX, textStart - 5.0f) == 0,
              "click left of the text start returns offset 0");
        Check(Hit(f, font, kSize, rectX, 0.0f) == 0,
              "click far to the left of the field returns offset 0");
    }

    // 3. A click far past the end lands on the end-of-text caret (value.size()).
    {
        InputField f; f.value = "Hello";
        float fullW = static_cast<float>(font.GetLineWidth(f.value, kSize));
        Check(Hit(f, font, kSize, rectX, textStart + fullW + 1000.0f) == f.value.size(),
              "click far past the end returns value.size()");
    }

    // 4. An empty field always resolves to offset 0.
    {
        InputField f; f.value = "";
        Check(Hit(f, font, kSize, rectX, textStart + 50.0f) == 0,
              "empty field returns offset 0 regardless of click x");
    }

    // 5. Clicking just past a glyph's right edge selects that glyph's boundary
    //    (cursor sits after the glyph). Walk the boundaries of an ASCII string
    //    and click slightly past each cumulative advance.
    {
        std::string_view s = "abcXYZ";
        InputField f; f.value = std::string(s);
        auto bounds = Boundaries(s);
        bool edgeOk = true;
        for (std::size_t k = 1; k < bounds.size(); ++k) {
            float w = static_cast<float>(font.GetLineWidth(s.substr(0, bounds[k]), kSize));
            // Click a hair before the glyph's right edge → nearest boundary is
            // this one (its advance is the closest of all boundaries).
            std::size_t got = Hit(f, font, kSize, rectX, textStart + w - 0.5f);
            if (got != bounds[k]) {
                edgeOk = false;
                std::println("  edge mismatch at boundary {} (w={}): got {}",
                             bounds[k], w, got);
            }
        }
        Check(edgeOk, "clicking near a glyph's right edge resolves to that glyph's boundary");
    }

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