/*
 * Copyright 2023 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/SkBitmap.h"
#include "include/gpu/graphite/Context.h"
#include "include/gpu/graphite/Recorder.h"
#include "src/gpu/GpuTypesPriv.h"
#include "src/gpu/graphite/Caps.h"
#include "src/gpu/graphite/ContextPriv.h"
#include "src/gpu/graphite/ProxyCache.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/Texture.h"
#include "src/gpu/graphite/TextureProxy.h"
#include "tools/DecodeUtils.h"
#include "tools/Resources.h"
#include "tools/graphite/GraphiteTestContext.h"

#include <thread>

namespace skgpu::graphite {

// This test exercises the basic MessageBus behavior of the ProxyCache by manually inserting an
// SkBitmap into the proxy cache and then changing its contents. This simple test should create
// an IDChangeListener that will remove the entry in the cache when the bitmap is changed and
// the resulting message processed.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest1, r, context, CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    SkBitmap bitmap;
    bool success = ToolUtils::GetResourceAsBitmap("images/mandrill_128.png", &bitmap);
    REPORTER_ASSERT(r, success);
    if (!success) {
        return;
    }

    REPORTER_ASSERT(r, proxyCache->numCached() == 0);

    sk_sp<TextureProxy> proxy = proxyCache->findOrCreateCachedProxy(recorder.get(), bitmap,
                                                                    "ProxyCacheTestTexture");

    REPORTER_ASSERT(r, proxyCache->numCached() == 1);

    bitmap.eraseColor(SK_ColorBLACK);

    proxyCache->forceProcessInvalidKeyMsgs();

    REPORTER_ASSERT(r, proxyCache->numCached() == 0);
}

// This test checks that, if the same bitmap is added to two separate ProxyCaches, when it is
// changed, both of the ProxyCaches will receive the message.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest2, r, context, CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder1 = context->makeRecorder();
    ProxyCache* proxyCache1 = recorder1->priv().proxyCache();
    std::unique_ptr<Recorder> recorder2 = context->makeRecorder();
    ProxyCache* proxyCache2 = recorder2->priv().proxyCache();

    SkBitmap bitmap;
    bool success = ToolUtils::GetResourceAsBitmap("images/mandrill_128.png", &bitmap);
    REPORTER_ASSERT(r, success);
    if (!success) {
        return;
    }

    REPORTER_ASSERT(r, proxyCache1->numCached() == 0);
    REPORTER_ASSERT(r, proxyCache2->numCached() == 0);

    sk_sp<TextureProxy> proxy1 = proxyCache1->findOrCreateCachedProxy(recorder1.get(), bitmap,
                                                                      "ProxyCacheTestTexture");
    sk_sp<TextureProxy> proxy2 = proxyCache2->findOrCreateCachedProxy(recorder2.get(), bitmap,
                                                                      "ProxyCacheTestTexture");

    REPORTER_ASSERT(r, proxyCache1->numCached() == 1);
    REPORTER_ASSERT(r, proxyCache2->numCached() == 1);

    bitmap.eraseColor(SK_ColorBLACK);

    proxyCache1->forceProcessInvalidKeyMsgs();
    proxyCache2->forceProcessInvalidKeyMsgs();

    REPORTER_ASSERT(r, proxyCache1->numCached() == 0);
    REPORTER_ASSERT(r, proxyCache2->numCached() == 0);
}

namespace {

struct ProxyCacheSetup {
    bool valid() const {
        return !fBitmap1.empty() && !fBitmap2.empty() && fProxy1 && fProxy2;
    }

    SkBitmap fBitmap1;
    sk_sp<TextureProxy> fProxy1;
    SkBitmap fBitmap2;
    sk_sp<TextureProxy> fProxy2;

    skgpu::StdSteadyClock::time_point fTimeBetweenProxyCreation;
    skgpu::StdSteadyClock::time_point fTimeAfterAllProxyCreation;
};

ProxyCacheSetup setup_test(Context* context,
                           skiatest::graphite::GraphiteTestContext* testContext,
                           Recorder* recorder,
                           skiatest::Reporter* r) {
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    ProxyCacheSetup setup;

    bool success1 = ToolUtils::GetResourceAsBitmap("images/mandrill_32.png", &setup.fBitmap1);
    bool success2 = ToolUtils::GetResourceAsBitmap("images/mandrill_64.png", &setup.fBitmap2);
    if (!success1 || !success2) {
        return {};
    }

    REPORTER_ASSERT(r, proxyCache->numCached() == 0);

    setup.fProxy1 = proxyCache->findOrCreateCachedProxy(recorder, setup.fBitmap1,
                                                        "ProxyCacheTestTexture");
    REPORTER_ASSERT(r, proxyCache->numCached() == 1);

    {
        // Ensure proxy1's Texture is created (and timestamped) at this time
        auto recording = recorder->snap();
        context->insertRecording({ recording.get() });
        context->submit(SyncToCpu::kYes);
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(2));
    setup.fTimeBetweenProxyCreation = skgpu::StdSteadyClock::now();
    std::this_thread::sleep_for(std::chrono::milliseconds(2));

    setup.fProxy2 = proxyCache->findOrCreateCachedProxy(recorder, setup.fBitmap2,
                                                        "ProxyCacheTestTexture");
    REPORTER_ASSERT(r, proxyCache->numCached() == 2);

    {
        // Ensure proxy2's Texture is created (and timestamped) at this time
        auto recording = recorder->snap();
        context->insertRecording({ recording.get() });
        testContext->syncedSubmit(context);
    }

    std::this_thread::sleep_for(std::chrono::milliseconds(2));
    setup.fTimeAfterAllProxyCreation = skgpu::StdSteadyClock::now();
    std::this_thread::sleep_for(std::chrono::milliseconds(2));

    return setup;
}

} // anonymous namespace

// This test exercises the ProxyCache's freeUniquelyHeld method.
DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest4,
                                               r,
                                               context,
                                               testContext,
                                               true,
                                               CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    ProxyCacheSetup setup = setup_test(context, testContext, recorder.get(), r);
    REPORTER_ASSERT(r, setup.valid());
    if (!setup.valid()) {
        return;
    }

    proxyCache->forceFreeUniquelyHeld();
    REPORTER_ASSERT(r, proxyCache->numCached() == 2);

    setup.fProxy1.reset();
    proxyCache->forceFreeUniquelyHeld();
    REPORTER_ASSERT(r, proxyCache->numCached() == 1);

    setup.fProxy2.reset();
    proxyCache->forceFreeUniquelyHeld();
    REPORTER_ASSERT(r, proxyCache->numCached() == 0);
}

// This test exercises the ProxyCache's purgeProxiesNotUsedSince method.
DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest5,
                                               r,
                                               context,
                                               testContext,
                                               true,
                                               CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    ProxyCacheSetup setup = setup_test(context, testContext, recorder.get(), r);
    REPORTER_ASSERT(r, setup.valid());
    if (!setup.valid()) {
        return;
    }

    REPORTER_ASSERT(r, !setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, !setup.fProxy2->texture()->testingShouldDeleteASAP());

    proxyCache->forcePurgeProxiesNotUsedSince(setup.fTimeBetweenProxyCreation);
    REPORTER_ASSERT(r, proxyCache->numCached() == 1);
    REPORTER_ASSERT(r, setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, !setup.fProxy2->texture()->testingShouldDeleteASAP());

    sk_sp<TextureProxy> test = proxyCache->find(setup.fBitmap1);
    REPORTER_ASSERT(r, !test);   // proxy1 should've been purged

    proxyCache->forcePurgeProxiesNotUsedSince(setup.fTimeAfterAllProxyCreation);
    REPORTER_ASSERT(r, proxyCache->numCached() == 0);
    REPORTER_ASSERT(r, setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, setup.fProxy2->texture()->testingShouldDeleteASAP());
}

// This test simply verifies that the ProxyCache is correctly updating the Resource's
// last access time stamp.
DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest6,
                                               r,
                                               context,
                                               testContext,
                                               true,
                                               CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    ProxyCacheSetup setup = setup_test(context, testContext, recorder.get(), r);
    REPORTER_ASSERT(r, setup.valid());
    if (!setup.valid()) {
        return;
    }

    REPORTER_ASSERT(r, !setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, !setup.fProxy2->texture()->testingShouldDeleteASAP());

    // update proxy1's timestamp
    sk_sp<TextureProxy> test = proxyCache->findOrCreateCachedProxy(recorder.get(), setup.fBitmap1,
                                                                   "ProxyCacheTestTexture");
    REPORTER_ASSERT(r, test == setup.fProxy1);

    std::this_thread::sleep_for(std::chrono::milliseconds(2));
    auto timeAfterProxy1Update = skgpu::StdSteadyClock::now();

    proxyCache->forcePurgeProxiesNotUsedSince(setup.fTimeBetweenProxyCreation);
    REPORTER_ASSERT(r, proxyCache->numCached() == 2);
    REPORTER_ASSERT(r, !setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, !setup.fProxy2->texture()->testingShouldDeleteASAP());

    proxyCache->forcePurgeProxiesNotUsedSince(setup.fTimeAfterAllProxyCreation);
    REPORTER_ASSERT(r, proxyCache->numCached() == 1);
    REPORTER_ASSERT(r, !setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, setup.fProxy2->texture()->testingShouldDeleteASAP());

    test = proxyCache->find(setup.fBitmap2);
    REPORTER_ASSERT(r, !test);   // proxy2 should've been purged

    proxyCache->forcePurgeProxiesNotUsedSince(timeAfterProxy1Update);
    REPORTER_ASSERT(r, proxyCache->numCached() == 0);
    REPORTER_ASSERT(r, setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, setup.fProxy2->texture()->testingShouldDeleteASAP());
}

// Verify that the ProxyCache's purgeProxiesNotUsedSince method can clear out multiple proxies.
DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest7,
                                               r,
                                               context,
                                               testContext,
                                               true,
                                               CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    ProxyCacheSetup setup = setup_test(context, testContext, recorder.get(), r);
    REPORTER_ASSERT(r, setup.valid());
    if (!setup.valid()) {
        return;
    }

    REPORTER_ASSERT(r, !setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, !setup.fProxy2->texture()->testingShouldDeleteASAP());

    proxyCache->forcePurgeProxiesNotUsedSince(setup.fTimeAfterAllProxyCreation);
    REPORTER_ASSERT(r, proxyCache->numCached() == 0);
    REPORTER_ASSERT(r, setup.fProxy1->texture()->testingShouldDeleteASAP());
    REPORTER_ASSERT(r, setup.fProxy2->texture()->testingShouldDeleteASAP());
}

// Verify that the ProxyCache's freeUniquelyHeld behavior is working in the ResourceCache.
DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest8,
                                               r,
                                               context,
                                               testContext,
                                               true,
                                               CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ResourceCache* resourceCache = recorder->priv().resourceCache();
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    resourceCache->setMaxBudget(0);

    ProxyCacheSetup setup = setup_test(context, testContext, recorder.get(), r);
    REPORTER_ASSERT(r, setup.valid());
    if (!setup.valid()) {
        return;
    }

    resourceCache->forcePurgeAsNeeded();

    REPORTER_ASSERT(r, proxyCache->numCached() == 2);

    setup.fProxy1.reset();
    proxyCache->forceProcessInvalidKeyMsgs();

    // unreffing fProxy1 and forcing message processing shouldn't purge proxy1 from the cache
    sk_sp<TextureProxy> test = proxyCache->find(setup.fBitmap1);
    REPORTER_ASSERT(r, test);
    test.reset();

    resourceCache->forcePurgeAsNeeded();

    REPORTER_ASSERT(r, proxyCache->numCached() == 1);
    test = proxyCache->find(setup.fBitmap1);
    REPORTER_ASSERT(r, !test);   // proxy1 should've been purged

    setup.fProxy2.reset();
    proxyCache->forceProcessInvalidKeyMsgs();

    // unreffing fProxy2 and forcing message processing shouldn't purge proxy2 from the cache
    test = proxyCache->find(setup.fBitmap2);
    REPORTER_ASSERT(r, test);
    test.reset();

    resourceCache->forcePurgeAsNeeded();

    REPORTER_ASSERT(r, proxyCache->numCached() == 0);
}

// Verify that the ProxyCache's purgeProxiesNotUsedSince behavior is working when triggered from
// ResourceCache.
DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest9,
                                               r,
                                               context,
                                               testContext,
                                               true,
                                               CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ResourceCache* resourceCache = recorder->priv().resourceCache();
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    ProxyCacheSetup setup = setup_test(context, testContext, recorder.get(), r);
    REPORTER_ASSERT(r, setup.valid());
    if (!setup.valid()) {
        return;
    }

    REPORTER_ASSERT(r, setup.fProxy1->isInstantiated());
    REPORTER_ASSERT(r, setup.fProxy2->isInstantiated());

    if (!setup.fProxy1->texture() || !setup.fProxy2->texture()) {
        return;
    }

    // Clear out resources used to setup bitmap proxies so we can track things easier.
    resourceCache->setMaxBudget(0);
    resourceCache->setMaxBudget(256 * (1 << 20));

    REPORTER_ASSERT(r, proxyCache->numCached() == 2);
    int baselineResourceCount = resourceCache->getResourceCount();
    // When buffer maps are async it can take extra time for buffers to be returned to the cache.
    if (context->priv().caps()->bufferMapsAreAsync()) {
        // We expect at least 2 textures (and possibly buffers).
        REPORTER_ASSERT(r, baselineResourceCount >= 2);
    } else {
        REPORTER_ASSERT(r, baselineResourceCount == 2);
    }
    // Force a command buffer ref on the second proxy in the cache so it can't be purged immediately
    setup.fProxy2->texture()->refCommandBuffer();

    Resource* proxy2ResourcePtr = setup.fProxy2->texture();

    setup.fProxy1.reset();
    setup.fProxy2.reset();
    REPORTER_ASSERT(r, proxyCache->numCached() == 2);

    auto timeAfterProxyCreation = skgpu::StdSteadyClock::now();

    // This should trigger both proxies to be purged from the ProxyCache. The first proxy should
    // immediately be purged from the ResourceCache as well since it has not other refs. The second
    // proxy will not be purged from the ResourceCache since it still has a command buffer ref.
    // However, that resource should have its deleteASAP flag set.
    resourceCache->purgeResourcesNotUsedSince(timeAfterProxyCreation);

    REPORTER_ASSERT(r, proxyCache->numCached() == 0);
    REPORTER_ASSERT(r, resourceCache->getResourceCount() == baselineResourceCount - 1);
    REPORTER_ASSERT(r, resourceCache->topOfPurgeableQueue() == nullptr);
    REPORTER_ASSERT(r, proxy2ResourcePtr->testingShouldDeleteASAP());

    // Removing the command buffer ref and returning proxy2Resource to the cache should cause it to
    // immediately get deleted without going in the purgeable queue.
    proxy2ResourcePtr->unrefCommandBuffer();
    resourceCache->forceProcessReturnedResources();

    REPORTER_ASSERT(r, proxyCache->numCached() == 0);
    REPORTER_ASSERT(r, resourceCache->getResourceCount() == baselineResourceCount - 2);
    REPORTER_ASSERT(r, resourceCache->topOfPurgeableQueue() == nullptr);
}

static sk_sp<TextureProxy> find_or_create_by_key(Recorder* recorder, int id, bool* regenerated) {
    *regenerated = false;

    skgpu::UniqueKey key;
    {
        static const skgpu::UniqueKey::Domain kTestDomain = UniqueKey::GenerateDomain();
        UniqueKey::Builder builder(&key, kTestDomain, 1, "TestExplicitKey");
        builder[0] = id;
    }

    struct Context {
        int id;
        bool* regenerated;
    } params { id, regenerated };

    return recorder->priv().proxyCache()->findOrCreateCachedProxy(
            recorder, key, &params,
            [](const void* context) {
                const Context* params = static_cast<const Context*>(context);
                *params->regenerated = true;

                SkBitmap bm;
                if (params->id == 1) {
                    if (!ToolUtils::GetResourceAsBitmap("images/mandrill_32.png", &bm)) {
                        return SkBitmap();
                    }
                } else if (!ToolUtils::GetResourceAsBitmap("images/mandrill_64.png", &bm)) {
                    return SkBitmap();
                }
                return bm;
            });
}

// Verify that the ProxyCache's explicit keying only generates the bitmaps as needed.
DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(ProxyCacheTest10,
                                               r,
                                               context,
                                               testContext,
                                               true,
                                               CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ProxyCache* proxyCache = recorder->priv().proxyCache();

    REPORTER_ASSERT(r, proxyCache->numCached() == 0);

    bool regenerated;
    sk_sp<TextureProxy> proxy1 = find_or_create_by_key(recorder.get(), 1, &regenerated);
    REPORTER_ASSERT(r, proxy1 && proxy1->dimensions().width() == 32);
    REPORTER_ASSERT(r, regenerated);

    sk_sp<TextureProxy> proxy2 = find_or_create_by_key(recorder.get(), 2, &regenerated);
    REPORTER_ASSERT(r, proxy2 && proxy2->dimensions().width() == 64);
    REPORTER_ASSERT(r, regenerated);

    REPORTER_ASSERT(r, proxyCache->numCached() == 2);

    // These cached proxies shouldn't be deleted because we hold local refs still
    proxyCache->forceFreeUniquelyHeld();
    REPORTER_ASSERT(r, proxyCache->numCached() == 2);

    // Cache hit should not invoke the bitmap generation function.
    sk_sp<TextureProxy> proxy1b = find_or_create_by_key(recorder.get(), 1, &regenerated);
    REPORTER_ASSERT(r, proxy1.get() == proxy1b.get());
    REPORTER_ASSERT(r, !regenerated);

    proxy1.reset();
    proxy1b.reset();
    proxy2.reset();
    (void) recorder->snap(); // Dump pending commands to release internal refs to the cached proxies

    // Now the cache should clean the cache entries up
    proxyCache->forceFreeUniquelyHeld();
    REPORTER_ASSERT(r, proxyCache->numCached() == 0);

    // And regeneration functions as expected
    proxy1 = find_or_create_by_key(recorder.get(), 1, &regenerated);
    REPORTER_ASSERT(r, proxy1 && proxy1->dimensions().width() == 32);
    REPORTER_ASSERT(r, regenerated);
}

}  // namespace skgpu::graphite
