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

#include "tests/Test.h"

#include "include/core/SkBlendMode.h"
#include "include/gpu/graphite/Recorder.h"
#include "src/gpu/SkBackingFit.h"
#include "src/gpu/graphite/ContextPriv.h"
#include "src/gpu/graphite/ContextUtils.h"
#include "src/gpu/graphite/Device.h"
#include "src/gpu/graphite/KeyContext.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/Renderer.h"
#include "src/gpu/graphite/TextureInfoPriv.h"
#include "src/gpu/graphite/TextureProxyView.h"

using namespace skgpu;
using namespace skgpu::graphite;

// These are unit tests that ensure the secondary "inner fill" draw is recorded when possible.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(InnerFillTest,
                                   reporter,
                                   context,
                                   CtsEnforcement::kApiLevel_202604) {
    if (context->priv().caps()->avoidDepthMode()) {
        SkDebugf("Skipping InnerFillTest under avoidDepthMode\n");
        return;
    }
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    sk_sp<Device> device = Device::Make(recorder.get(),
                                        SkImageInfo::Make(512, 512,
                                                          kRGBA_8888_SkColorType,
                                                          kPremul_SkAlphaType),
                                        Budgeted::kYes,
                                        Mipmapped::kNo,
                                        SkBackingFit::kExact,
                                        /*surfaceProps=*/{},
                                        LoadOp::kClear,
                                        "inner-fill");

    // Default rrect size is sufficient to avoid the small-size exclusion criteria
    auto testRRect = [&](const SkPaint& paint, bool innerFillExpected, float rrectSize = 128.f) {
        SkRRect rrect = SkRRect::MakeRectXY(SkRect::MakeXYWH(0.f, 0.f, rrectSize, rrectSize),
                                            0.1f * rrectSize, 0.1f * rrectSize);
        int initialRenderSteps = device->testingOnly_pendingRenderSteps();
        int initialTasks = recorder->priv().numRootTasks();
        device->drawRRect(rrect, paint);

        int actualRenderSteps = device->testingOnly_pendingRenderSteps();
        if (recorder->priv().numRootTasks() != initialTasks) {
            // Flushed for a dst read copy before appending the new draw and possibly inner fill.
            initialRenderSteps = 0;
        }

        // TODO(michaelludwig): After migrating to the layer draw tracking system, having a way to
        // traverse the recorded draws and inspect them could let these checks be more robust, e.g.
        // one of the steps is actually the CoverBoundsRenderStep AND it's in the front.
        int expectedRenderSteps = initialRenderSteps + (innerFillExpected ? 2 : 1);
        REPORTER_ASSERT(reporter, actualRenderSteps == expectedRenderSteps,
                        "Expected (%d) vs Actual (%d)", expectedRenderSteps, actualRenderSteps);
    };

    // ** Test cases that should not produce an inner fill:
    // 1. A transparent paint, so the draw is not eligible for an inner fill
    {
        skiatest::ReporterContext label{reporter, "transparent paint"};
        SkPaint transparentPaint;
        transparentPaint.setAlphaf(0.5f);
        testRRect(transparentPaint, /*innerFillExpected=*/false);
    }
    // 2. A very small rounded rectangle that would otherwise have an inner fill
    {
        skiatest::ReporterContext label{reporter, "small rrect"};
        testRRect(SkPaint(), /*innerFillExpected=*/false, /*rrectSize=*/8.f);
    }
    // 3. An opaque paint that has a blend mode that still mixes with the dst
    {
        skiatest::ReporterContext label{reporter, "fancy blend"};
        SkPaint opaqueBlendedPaint;
        opaqueBlendedPaint.setBlendMode(SkBlendMode::kSrcIn);
        testRRect(opaqueBlendedPaint, /*innerFillExpected=*/false);
    }

    // ** Test cases that shouldn't produce an inner fill due to analytic clipping
    device->pushClipStack();
    device->clipShader(SkShaders::Color({0.1f, 0.2f, 0.3f, 0.5f}, nullptr),
                        SkClipOp::kIntersect);
    // 4. An opaque paint but with an analytic clip
    {
        skiatest::ReporterContext label{reporter, "opaque src-over analytic clip"};
        testRRect(SkPaint(), /*innerFillExpected=*/false);
    }
    // 5. A kSrc paint but there is analytic clip
    {
        skiatest::ReporterContext label{reporter, "src w/ analytic clip"};
        SkPaint srcPaint;
        srcPaint.setBlendMode(SkBlendMode::kSrc);
        testRRect(srcPaint, /*innerFillExpected=*/false);
    }
    device->popClipStack();

    // ** Test cases that should produce an inner fill:
    // 1. A kSrcOver paint with opaque color (or shader)
    {
        skiatest::ReporterContext label{reporter, "opaque src-over"};
        testRRect(SkPaint(), /*innerFillExpected=*/true);
    }
    // 2. A kSrc paint when the device supports HW blending regardless of coverage
    {
        skiatest::ReporterContext label{reporter, "src"};
        SkPaint srcPaint;
        srcPaint.setBlendMode(SkBlendMode::kSrc);
        srcPaint.setAlphaf(0.5f); // emphasize it's src color is not opaque
        testRRect(srcPaint, /*innerFillExpected=*/true);
    }

    // We don't actually care about rendering, so just throw everything away
}

DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(OptimizeForOpacity,
                                   reporter,
                                   context,
                                   CtsEnforcement::kNextRelease) {
    static constexpr SkColor4f kSemiTransparent{1.f, 1.f, 1.f, 0.5f};
    static constexpr SkColor4f kOpaque{1.f, 1.f, 1.f, 1.f};

    const Caps* caps = context->priv().caps();
    std::unique_ptr<Recorder> recorder = context->makeRecorder();

    // We need a DrawContext to participate in uniform extraction and key generation
    SkColorInfo targetInfo{kRGBA_8888_SkColorType, kPremul_SkAlphaType, /*cs=*/nullptr};
    sk_sp<TextureProxy> target = TextureProxy::Make(
            caps, recorder->priv().resourceProvider(),
            /*dimensions=*/{16, 16},
            caps->getDefaultSampledTextureInfo(targetInfo.colorType(),
                                               Mipmapped::kNo,
                                               Protected::kNo,
                                               Renderable::kYes),
            "OptimizeForOpacityTarget",
            Budgeted::kYes);
    sk_sp<DrawContext> drawContext = DrawContext::Make(
            caps, std::move(target), {16, 16}, targetInfo, {});

    auto genPaintID = [&](const PaintParams& paint,
                          Coverage rendererCoverage,
                          SkEnumBitMask<DstUsage> expectedDstUsage,
                          const NonMSAAClip& clip={}) {
        ShadingParams shading{caps, paint, clip, /*clipShader=*/nullptr,
                              rendererCoverage, TextureFormat::kRGBA8};

        auto keyAndDataBuilder = recorder->priv().popOrCreateKeyAndDataBuilder();
        // This opts into kPreferFixedSrcBlend as if we were drawing a rect or quad that supports
        // inner fills (and assuming that `rendererCoverage` is set to match either the AA or the
        // pixel-aligned case appropriately).
        KeyContext keyContext{recorder.get(),
                              drawContext.get(),
                              &keyAndDataBuilder->second,
                              &keyAndDataBuilder->first,
                              SkM44(),
                              SkRect::MakeIWH(16, 16),
                              targetInfo,
                              KeyGenFlags::kPreferFixedSrcBlend,
                              paint.color()};

        auto [paintID, dstUsage] = *shading.toKey(keyContext);
        // The expected usages should be a subset of what was actually returned
        REPORTER_ASSERT(reporter, (dstUsage & expectedDstUsage) == expectedDstUsage);

        std::optional<UniquePaintParamsID> opaqueID;
        if (dstUsage == DstUsage::kNone || (dstUsage & DstUsage::kDstOnlyUsedByRenderer)) {
            opaqueID = shading.optimizeForOpacity(keyContext, paintID);
        }

        keyAndDataBuilder->first.resetForDraw();
        keyAndDataBuilder->second.resetForDraw();
        recorder->priv().pushKeyAndDataBuilder(std::move(keyAndDataBuilder));
        return std::make_pair(paintID, opaqueID);
    };

    // Construct an unclipped solid color + src paint without coverage, e.g. the optimized
    // paint params key for an inner fill.
    auto [nonAASrcID, opaqueNonAASrcID] =
            genPaintID(PaintParams{kSemiTransparent, SkBlendMode::kSrc},
                       Coverage::kNone,
                       DstUsage::kNone);
    REPORTER_ASSERT(reporter, opaqueNonAASrcID.has_value(), "Non-AA + kSrc not detected as opaque");
    REPORTER_ASSERT(reporter, nonAASrcID == *opaqueNonAASrcID,
                    "optimizeForOpacity() should be a no-op for non-AA+kSrc");

    // Case 1: src-over + opaque color without coverage is optimized up front to match kSrc
    {
        auto [nonAASrcOverID, opaqueNonAASrcOverID] =
                genPaintID(PaintParams{kOpaque, SkBlendMode::kSrcOver},
                           Coverage::kNone,
                           DstUsage::kNone);
        REPORTER_ASSERT(reporter, opaqueNonAASrcOverID.has_value(),
                        "Non-AA + kSrcOver not detected as opaque");
        REPORTER_ASSERT(reporter, nonAASrcOverID == *opaqueNonAASrcOverID,
                        "optimizeForOpacity() should be a no-op for non-AA+kSrcOver");
        REPORTER_ASSERT(reporter, nonAASrcOverID == nonAASrcID,
                        "Opaque paint ID should match between non-AA+kSrcOver and non-AA+kSrc");
    }

    // Case 2: src with coverage is optimized in second pass for inner fill, although the paint
    // ID works out the same (the change comes later with a different RenderStep combination).
    {
        auto [aaSrcID, opaqueAASrcID] =
                genPaintID(PaintParams{kOpaque, SkBlendMode::kSrc},
                           Coverage::kSingleChannel,
                           DstUsage::kDependsOnDst | DstUsage::kDstOnlyUsedByRenderer);
        REPORTER_ASSERT(reporter, opaqueAASrcID.has_value(),
                        "AA + kSrc should have opaque variant");
        REPORTER_ASSERT(reporter, aaSrcID == *opaqueAASrcID,
                        "optimizeForOpacity() should be a no-op for AA+kSrc");
        REPORTER_ASSERT(reporter, *opaqueAASrcID == nonAASrcID,
                        "Opaque paint ID should match between AA+kSrc and non-AA+kSrc");
    }

    // Case 3: src-over + opaque color with coverage is optimized in second pass for inner fill
    {
        auto [aaSrcOverID, opaqueAASrcOverID] =
                genPaintID(PaintParams{kOpaque, SkBlendMode::kSrcOver},
                           Coverage::kSingleChannel,
                           DstUsage::kDependsOnDst | DstUsage::kDstOnlyUsedByRenderer);
        REPORTER_ASSERT(reporter, opaqueAASrcOverID.has_value(),
                        "AA + kSrcOver should have opaque variant");
        REPORTER_ASSERT(reporter, aaSrcOverID != *opaqueAASrcOverID,
                        "optimizeForOpacity() should be different for AA+kSrcOver");
        REPORTER_ASSERT(reporter, *opaqueAASrcOverID == nonAASrcID,
                        "Opaque paint ID should match between AA+kSrcOver and non-AA+kSrc");
    }

    // Case 4: src-over + transparent color is not optimized and has no inner fill follow-up
    {
        auto [_, opaqueAASrcOverID] =
                genPaintID(PaintParams{kSemiTransparent, SkBlendMode::kSrcOver},
                           Coverage::kSingleChannel,
                           DstUsage::kDependsOnDst);
        REPORTER_ASSERT(reporter, !opaqueAASrcOverID.has_value(),
                        "AA + kSrcOver should not have an opaque variant");
    }

    AnalyticClip clip;
    clip.fBounds = { 1.f, 1.f, 15.f, 15.f };

    // Case 5: src without coverage but an analytic clip cannot have an inner fill
    {
        auto [_, clippedOpaqueNonAASrcID] =
        genPaintID(PaintParams{kSemiTransparent, SkBlendMode::kSrc},
                    Coverage::kNone,
                    DstUsage::kDependsOnDst,
                    NonMSAAClip{clip, {}});
        REPORTER_ASSERT(reporter, !clippedOpaqueNonAASrcID.has_value(),
                        "Non-AA + kSrc + AnalyticClip should not have an opaque variant");
    }
    // Case 6: src-over + opaque without coverage but an analytic clip cannot have an inner fill
    {
        auto [_, clippedOpaqueNonAASrcOverID] =
                genPaintID(PaintParams{kOpaque, SkBlendMode::kSrcOver},
                           Coverage::kNone,
                           DstUsage::kDependsOnDst,
                           NonMSAAClip{clip, {}});
        REPORTER_ASSERT(reporter, !clippedOpaqueNonAASrcOverID.has_value(),
                        "Non-AA + kSrcOver + AnalyticClip should not have an opaque variant");
    }
    // Case 7: src with coverage and an analytic clip cannot have an inner fill
    {
        auto [_, clippedOpaqueAASrcID] =
                genPaintID(PaintParams{kOpaque, SkBlendMode::kSrc},
                           Coverage::kSingleChannel,
                           DstUsage::kDependsOnDst,
                           NonMSAAClip{clip, {}});
        REPORTER_ASSERT(reporter, !clippedOpaqueAASrcID.has_value(),
                        "AA + kSrc + AnalyticClip should not have an opaque variant");
    }
    // Case 8: src-over + opaque with coverage and an analytic clip cannot have an inner fill
    {
        auto [_, clippedOpaqueAASrcOverID] =
                genPaintID(PaintParams{kOpaque, SkBlendMode::kSrcOver},
                           Coverage::kSingleChannel,
                           DstUsage::kDependsOnDst,
                           NonMSAAClip{clip, {}});
        REPORTER_ASSERT(reporter, !clippedOpaqueAASrcOverID.has_value(),
                        "AA + kSrcOver + AnalyticClip should not have an opaque variant");
    }
}
