/*
 * Copyright 2025 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "include/core/SkPathBuilder.h"
#include "include/core/SkPathTypes.h"
#include "src/core/SkPathData.h"
#include "src/core/SkPathPriv.h"

#include "tests/Test.h"

#include <functional>
#include <limits>

namespace {
template <typename T> bool spaneq(SkSpan<T> a, SkSpan<T> b) {
    if (a.size() != b.size()) {
        return false;
    }
    for (size_t i = 0; i < a.size(); ++i) {
        if (a[i] != b[i]) {
            return false;
        }
    }
    return true;
}
}

DEF_TEST(pathdata_empty, reporter) {
    auto pdata = SkPathData::Empty();

    REPORTER_ASSERT(reporter, pdata->empty());
    REPORTER_ASSERT(reporter, pdata->points().empty());
    REPORTER_ASSERT(reporter, pdata->conics().empty());
    REPORTER_ASSERT(reporter, pdata->verbs().empty());

    REPORTER_ASSERT(reporter, pdata->bounds() == SkRect::MakeEmpty());
    REPORTER_ASSERT(reporter, pdata->segmentMask() == 0);

    REPORTER_ASSERT(reporter, !pdata->asLine());
    REPORTER_ASSERT(reporter, !pdata->asOval());
    REPORTER_ASSERT(reporter, !pdata->asRect());
    REPORTER_ASSERT(reporter, !pdata->asRRect());

    auto xformed = pdata->makeTransform(SkMatrix::Scale(2, 3));
    REPORTER_ASSERT(reporter, *pdata == *xformed);
}

using IsAPredicate = bool(const SkPathData&);

static void check_asA_transforms(skiatest::Reporter* reporter, sk_sp<SkPathData> orig,
                                 IsAPredicate returns_asA) {
    SkASSERT(returns_asA(*orig));

    const struct {
        SkMatrix mx;
        bool     expectedAsA;
    } gPairs[] = {
        { SkMatrix::I(),             true },
        { SkMatrix::Translate(1, 2), true },
        { SkMatrix::Scale(2, 3),     true },
        { SkMatrix::RotateDeg(30),  false },
    };

    for (const auto& pair : gPairs) {
        auto pdata = orig->makeTransform(pair.mx);
        REPORTER_ASSERT(reporter, returns_asA(*pdata) == pair.expectedAsA);
    }
}

/*
 *  Differe ways to "make" a rectangular PathData
 */
using RectMaker = sk_sp<SkPathData>(const SkRect&, SkPathDirection);

static sk_sp<SkPathData> factory_rect(const SkRect& r, SkPathDirection d) {
    return SkPathData::Rect(r, d);
}
static sk_sp<SkPathData> poly4_rect(const SkRect& r, SkPathDirection d) {
    std::array<SkPoint, 4> pts = r.toQuad(d);
    return SkPathData::Polygon(pts, true);
}
static sk_sp<SkPathData> poly5_rect(const SkRect& r, SkPathDirection d) {
    std::array<SkPoint, 5> pts;
    r.copyToQuad(pts, d);
    pts[4] = pts[0];    // explicly add the closing line
    return SkPathData::Polygon(pts, true);
}
static sk_sp<SkPathData> builder_rect_rect(const SkRect& r, SkPathDirection d) {
    SkPathBuilder bu;
    bu.addRect(r, d);
    return bu.detachData();
}
static sk_sp<SkPathData> builder_poly4_rect(const SkRect& r, SkPathDirection d) {
    std::array<SkPoint, 4> pts = r.toQuad(d);
    SkPathBuilder bu;
    bu.addPolygon(pts, true);
    return bu.detachData();
}
static sk_sp<SkPathData> builder_poly5_rect(const SkRect& r, SkPathDirection d) {
    std::array<SkPoint, 5> pts;
    r.copyToQuad(pts, d);
    pts[4] = pts[0];    // explicly add the closing line
    SkPathBuilder bu;
    bu.addPolygon(pts, true);
    return bu.detachData();
}

RectMaker* const gRectMakers[] = {
    factory_rect,
    poly4_rect,
    poly5_rect,
    builder_rect_rect,
    builder_poly4_rect,
    builder_poly5_rect,
};

DEF_TEST(pathdata_rect, reporter) {
    const SkRect r = {1, 2, 3, 4};

    auto sign = [](float x) -> float {
        if (x == 0) {
            return 0;
        } else {
            return x > 0 ? 1 : -1;
        }
    };

    for (auto maker : gRectMakers) {
        for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
            auto pdata = maker(r, dir);
            REPORTER_ASSERT(reporter, r == pdata->bounds());

            // 1. manually determine if *we* think it is a rect

            const SkSpan<const SkPoint> pts = pdata->points();
            REPORTER_ASSERT(reporter, r == SkRect::Bounds(pts).value());

            const float crossSign = (dir == SkPathDirection::kCW) ? 1 : -1;
            for (int i = 1; i < 3; ++i) {
                SkVector u = pts[i]   - pts[i-1],
                         v = pts[i+1] - pts[i];
                const float cross = u.cross(v),
                            dot   = u.dot(v);
                REPORTER_ASSERT(reporter, dot == 0);
                REPORTER_ASSERT(reporter, crossSign == sign(cross));
            }

            // 2. now ask the pathdata

            auto isa = pdata->asRect();
            REPORTER_ASSERT(reporter, isa.has_value());
            REPORTER_ASSERT(reporter, isa->fRect == r);
            REPORTER_ASSERT(reporter, isa->fDirection == dir);
            REPORTER_ASSERT(reporter, isa->fStartIndex == 0);

            check_asA_transforms(reporter, pdata, [](const SkPathData& pd) {
                return pd.asRect().has_value();
            });
        }
    }
}

static sk_sp<SkPathData> factory_poly(SkSpan<const SkPoint> pts, bool isClosed) {
    return SkPathData::Polygon(pts, isClosed);
}
static sk_sp<SkPathData> builder_poly(SkSpan<const SkPoint> pts, bool isClosed) {
    SkPathBuilder bu;
    bu.addPolygon(pts, isClosed);
    return bu.detachData();
}

DEF_TEST(pathdata_polygon, reporter) {
    const SkPoint points[] = {
        {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9},
    };

    for (auto maker : {factory_poly, builder_poly}) {
        for (auto isClosed : {false, true}) {
            for (size_t n = 0; n <= std::size(points); ++n) {
                const SkSpan<const SkPoint> pts = {points, n};
                auto pdata = maker(pts, isClosed);

                const bool shouldBeEmpty = isClosed ? n == 0 : n <= 1;
                if (shouldBeEmpty) {
                    REPORTER_ASSERT(reporter, pdata->empty());
                    continue;
                }

                REPORTER_ASSERT(reporter, spaneq(pdata->points(), pts));
                REPORTER_ASSERT(reporter, pdata->conics().empty());

                auto line = pdata->asLine();
                if (n == 2 && !isClosed) {
                    REPORTER_ASSERT(reporter, line.has_value());
                    REPORTER_ASSERT(reporter, line.value()[0] == points[0]);
                    REPORTER_ASSERT(reporter, line.value()[1] == points[1]);

                    auto pline = SkPathData::Line(points[0], points[1]);
                    REPORTER_ASSERT(reporter, *pline == *pdata);
                } else {
                    REPORTER_ASSERT(reporter, !line.has_value());
                }

                const size_t expectedVerbs = pts.size() + isClosed;
                auto vbs = pdata->verbs();
                REPORTER_ASSERT(reporter, vbs.size() == expectedVerbs);

                REPORTER_ASSERT(reporter, vbs[0] == SkPathVerb::kMove);
                for (size_t i = 1; i < pts.size(); ++i) {
                    REPORTER_ASSERT(reporter, vbs[i] == SkPathVerb::kLine);
                }
                if (isClosed) {
                    REPORTER_ASSERT(reporter, vbs.back() == SkPathVerb::kClose);
                }
            }
        }
    }
}

static sk_sp<SkPathData> factory_oval(const SkRect& r, SkPathDirection dir, unsigned start) {
    return SkPathData::Oval(r, dir, start);
}
static sk_sp<SkPathData> builder_oval(const SkRect& r, SkPathDirection dir, unsigned start) {
    SkPathBuilder bu;
    bu.addOval(r, dir, start);
    return bu.detachData();
}

DEF_TEST(pathdata_oval, reporter) {
    const SkRect bounds = {1, 2, 3, 4};
    const unsigned kStartIndexCount = 4;

    for (auto maker : {factory_oval, builder_oval}) {
        for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
            for (unsigned start = 0; start < kStartIndexCount; ++start) {
                auto pdata = maker(bounds, dir, start);

                REPORTER_ASSERT(reporter, pdata->bounds() == bounds);

                auto oval = pdata->asOval();
                REPORTER_ASSERT(reporter, oval.has_value());
                REPORTER_ASSERT(reporter, oval->fBounds == bounds);
                REPORTER_ASSERT(reporter, oval->fDirection == dir);
                REPORTER_ASSERT(reporter, oval->fStartIndex == start);

                check_asA_transforms(reporter, pdata, [](const SkPathData& pd) {
                    return pd.asOval().has_value();
                });
            }
        }
    }
}

static sk_sp<SkPathData> factory_rrect(const SkRRect& r, SkPathDirection dir, unsigned start) {
    return SkPathData::RRect(r, dir, start);
}
static sk_sp<SkPathData> builder_rrect(const SkRRect& r, SkPathDirection dir, unsigned start) {
    SkPathBuilder bu;
    bu.addRRect(r, dir, start);
    return bu.detachData();
}

DEF_TEST(pathdata_rrect, reporter) {
    const SkRect bounds = {0, 0, 20, 30};
    const SkRRect rrect = SkRRect::MakeRectXY(bounds, 2, 3);
    const unsigned kStartIndexCount = 8;

    for (auto maker : {factory_rrect, builder_rrect}) {
        for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
            for (unsigned start = 0; start < kStartIndexCount; ++start) {
                auto pdata = maker(rrect, dir, start);

                REPORTER_ASSERT(reporter, pdata->bounds() == bounds);

                auto rr = pdata->asRRect();
                REPORTER_ASSERT(reporter, rr.has_value());
                REPORTER_ASSERT(reporter, rr->fRRect == rrect);
                REPORTER_ASSERT(reporter, rr->fDirection == dir);
                REPORTER_ASSERT(reporter, rr->fStartIndex == start);

                check_asA_transforms(reporter, pdata, [](const SkPathData& pd) {
                    return pd.asRRect().has_value();
                });
            }
        }
    }
}

DEF_TEST(pathdata_make_edgecases, reporter) {

    // just create some points for our tests
    SkPoint pts[20];
    for (size_t i = 0; i < std::size(pts); ++i) {
        pts[i] = {i * 1.0f, i * 1.0f };
    }
    const float conicWeights[] = {1.5f, 2, 3};

    constexpr SkPathVerb M = SkPathVerb::kMove,
                         L = SkPathVerb::kLine,
                         Q = SkPathVerb::kQuad,
                         K = SkPathVerb::kConic,
                         C = SkPathVerb::kCubic,
                         X = SkPathVerb::kClose;

    // only these two sequence will result in an "empty" PathData

    const SkPathVerb move[] = { M };  // the M will not be trimmed

    REPORTER_ASSERT(reporter, SkPathData::Make({}, {}, {})->empty());
    REPORTER_ASSERT(reporter, !SkPathData::Make({pts, 1}, move, {})->empty());

    // these sequenes are all illegal (bad verb sequencing)

    const SkPathVerb bad0[] = { L };            // didn't start with M
    const SkPathVerb bad1[] = { M, M, L };      // consecutive Ms
    const SkPathVerb bad2[] = { M, L, X, X};    // consecutive Xs
    const SkPathVerb bad3[] = { M, L, M, M};    // consecutive Ms

    REPORTER_ASSERT(reporter, SkPathData::Make({pts, 1}, bad0, {}) == nullptr);
    REPORTER_ASSERT(reporter, SkPathData::Make({pts, 3}, bad1, {}) == nullptr);
    REPORTER_ASSERT(reporter, SkPathData::Make({pts, 2}, bad2, {}) == nullptr);
    REPORTER_ASSERT(reporter, SkPathData::Make({pts, 4}, bad3, {}) == nullptr);

    // Odd but legal, the trailing M is preserved, but not part of the bounds

    const SkPathVerb trimmed[] = { M, L, M }; //legal
    auto pdata = SkPathData::Make({pts, 3}, trimmed, {});

    REPORTER_ASSERT(reporter, pdata->points().size() == 3);
    REPORTER_ASSERT(reporter, pdata->verbs().size() == 3);
    REPORTER_ASSERT(reporter, pdata->bounds() == SkRect::Bounds({pts, 2}).value());

    // Now check on # of points and conic weights

    const SkPathVerb verbs[] = { M, L, Q, K, C, X, M };    // 1+1+2+2+3+0+1 = 10 + 1 conic weight

    const struct {
        size_t nPts, nConics;
        bool success;
    } combos[] = {
        { 10, 1, true  },   // just right
        {  9, 1, false },   // not enough points
        { 11, 1, false },   // too many points
        { 10, 0, false },   // not enough conics
        { 10, 2, false },   // too many conics
        {  0, 1, false },   // degenerate, should not crash on moveto trim
    };
    for (auto c : combos) {
        pdata = SkPathData::Make({pts, c.nPts}, verbs, {conicWeights, c.nConics});
        if (c.success) {
            REPORTER_ASSERT(reporter, pdata != nullptr);
        } else {
            REPORTER_ASSERT(reporter, pdata == nullptr);
        }
    }
}

static inline std::optional<SkRRect> make_bad_rrect() {
    constexpr float big = std::numeric_limits<float>::max();
    SkRRect rr = SkRRect::MakeRectXY({0, 0, big, big}, 4, 4);
    rr.offset(big, big);
    if (!rr.rect().isFinite()) {
        return rr;
    }
    return {};  // failed to make a non-finite rrect
}

/*
 *  Test that we cannot make a non-finite PathData
 */
DEF_TEST(pathdata_make_nonfinite, reporter) {
    const float inf = SK_FloatInfinity;

    SkPoint pts[] = {
        {0, 0}, {inf, 1}, {2, 4},
    };
    SkPathVerb vbs[] = {
        SkPathVerb::kMove, SkPathVerb::kConic,
    };
    float weights[] = { 2 };

    sk_sp<SkPathData> pdata = SkPathData::Make(pts, vbs, weights);
    REPORTER_ASSERT(reporter, pdata == nullptr);

    pts[1].fX = 3;  // remove non-finite from pts
    const float badWValues[] = { -1, inf, -inf, inf * 0 /* nan */ };
    for (auto bad : badWValues) {
        weights[0] = bad;
        REPORTER_ASSERT(reporter, SkPathData::Make(pts, vbs, weights) == nullptr);
    }

    SkRect r = {1, 2, inf, 4};
    REPORTER_ASSERT(reporter, SkPathData::Rect(r) == nullptr);
    REPORTER_ASSERT(reporter, SkPathData::Oval(r) == nullptr);

    // Most RRect methods 'sanitize' the values before returning the RRect, so it hard to
    // actually make one for testing. If our attempt suceeds, we will test with it.
    if (auto rr = make_bad_rrect()) {
        REPORTER_ASSERT(reporter, SkPathData::RRect(*rr) == nullptr);
    }

    pts[1].fX = inf;  // restore non-finite value
    REPORTER_ASSERT(reporter, SkPathData::Polygon(pts, false) == nullptr);

    {
        // Non-finite trailing moves should also be rejected.
        SkPathVerb v[] = { SkPathVerb::kMove, SkPathVerb::kLine, SkPathVerb::kMove };
        SkPoint    p[] = { {0, 0}, {10, 10}, {inf, 20} };
        REPORTER_ASSERT(reporter, SkPathData::Make(p, v) == nullptr);
    }
}

DEF_TEST(pathdata_transform, reporter) {
    SkMatrix mx;
    const SkRect r = {10, 20, 30, 40};
    auto data = SkPathData::Oval(r);

    mx = SkMatrix::I();
    auto newd = data->makeTransform(mx);
    REPORTER_ASSERT(reporter, *newd == *data);

    mx = SkMatrix::Translate(5, 6);
    newd = data->makeTransform(mx);
    REPORTER_ASSERT(reporter, newd->bounds() == r.makeOffset(5, 6));

    mx = SkMatrix::Scale(0.5f, 2);
    newd = data->makeTransform(mx);
    SkRect r2 = {
        r.fLeft * 0.5f,
        r.fTop * 2,
        r.fRight * 0.5f,
        r.fBottom * 2,
    };
    REPORTER_ASSERT(reporter, newd->bounds() == r2);

    mx = SkMatrix::Scale(SK_FloatInfinity, 2);
    newd = data->makeTransform(mx);
    REPORTER_ASSERT(reporter, newd == nullptr);

    mx = SkMatrix::Scale(SK_ScalarNaN, 2);
    newd = data->makeTransform(mx);
    REPORTER_ASSERT(reporter, newd == nullptr);
}

/*
 *  This tests how convexity is tracked under transformation
 *  1. unknown stays unknown (we don't actively compute convexity)
 *  2. concave stays concave
 *  3. convex ... may stay convex -- it depends if we feel it is (numerically) safe.
 *     See SkPathPriv::TransformConvexity() for the current heuristics.
 *  4. The (above) helper is shared with SkPath::transform(), so it and SkPathData
 *     should handle transforms + convexity the same.
 */
DEF_TEST(pathdata_transform_convexity, reporter) {
    const SkPoint pts[] = {
        {0, 0}, {100, 0}, {200, 0}, {200, 200},
    };
    // needed late for our assumpts about convexity preservation
    REPORTER_ASSERT(reporter, SkPathPriv::IsAxisAligned(pts));

    auto src = SkPathData::Polygon(pts, true);
    auto convexity = SkPathPriv::GetConvexityOrUnknown(*src);

    // don't do any work we didn't ask for
    REPORTER_ASSERT(reporter, convexity == SkPathConvexity::kUnknown);
    auto raw = src->raw(SkPathFillType::kDefault, SkResolveConvexity::kNo);
    REPORTER_ASSERT(reporter, raw.fConvexity == SkPathConvexity::kUnknown);
    // now ask for it
    raw = src->raw(SkPathFillType::kDefault, SkResolveConvexity::kYes);
    REPORTER_ASSERT(reporter, raw.isKnownToBeConvex());

    // For these matrices, given that our points are axis-aligned, we should be able
    // to preserve whatever convexity our src has.

    const SkMatrix safeMatrices[] = {
        SkMatrix(), SkMatrix::Translate(1, 2), SkMatrix::Scale(2, 3),
    };
    const SkPathConvexity convexities[] = {
        SkPathConvexity::kUnknown,
        SkPathConvexity::kConvex_CW,    // matches our test data
        SkPathConvexity::kConcave,
    };
    for (const auto& mx : safeMatrices) {
        for (auto conv : convexities) {
            raw.fConvexity = conv;
            auto dst = SkPathData::MakeTransform(raw, mx);
            convexity = SkPathPriv::GetConvexityOrUnknown(*dst);
            REPORTER_ASSERT(reporter, convexity == conv);
        }
    }

    // for this matrix, we do not expect to preserve convexity
    // (since we don't choose to actually compute convexity at this stage)
    SkMatrix mx = SkMatrix::RotateDeg(30);
    for (auto conv : convexities) {
        raw.fConvexity = conv;
        auto dst = SkPathData::MakeTransform(raw, mx);
        convexity = SkPathPriv::GetConvexityOrUnknown(*dst);
        auto expected = SkPathConvexity_IsConvex(conv) ? SkPathConvexity::kUnknown
                                                       : conv;
        REPORTER_ASSERT(reporter, convexity == expected);
    }
}

DEF_TEST(pathdata_inverted_bounds, reporter) {
    using makerT = std::function<sk_sp<SkPathData>(const SkRect&)>;
    const auto check = [&reporter](const makerT& maker) {
        constexpr SkRect bounds = {-10, -10, 10, 10};
        constexpr SkRect inverted_bounds = {10, 10, -10, -10};
        REPORTER_ASSERT(reporter, maker(bounds)->bounds() == bounds);
        REPORTER_ASSERT(reporter, maker(inverted_bounds)->bounds() == bounds);
    };

    {
        for (auto maker : {factory_rect, builder_rect_rect}) {
            for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
                check([&maker, dir](const SkRect& r) { return maker(r, dir); });
            }
        }
    }

    {
        constexpr unsigned kStartIndexCount = 4;
        for (auto maker : {factory_oval, builder_oval}) {
            for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
                for (unsigned start = 0; start < kStartIndexCount; ++start) {
                    check([&maker, dir, start](const SkRect& r) { return maker(r, dir, start); });
                }
            }
        }
    }

    {
        constexpr unsigned kStartIndexCount = 8;
        for (auto maker : {factory_rrect, builder_rrect}) {
            for (auto dir : {SkPathDirection::kCW, SkPathDirection::kCCW}) {
                for (unsigned start = 0; start < kStartIndexCount; ++start) {
                    for (int sign : {1, -1}) {
                        check([&maker, dir, start, sign](const SkRect& r) {
                            const SkRRect rrect = SkRRect::MakeRectXY(r, sign * 2, sign * 3);
                            return maker(rrect, dir, start);
                        });
                    }
                }
            }
        }
    }
}
