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

#ifndef skgpu_graphite_Resource_DEFINED
#define skgpu_graphite_Resource_DEFINED

#include "include/gpu/GpuTypes.h"
#include "include/private/base/SkMutex.h"
#include "src/gpu/GpuTypesPriv.h"
#include "src/gpu/graphite/GraphiteResourceKey.h"
#include "src/gpu/graphite/ResourceTypes.h"

#include <atomic>

class SkMutex;

namespace skgpu::graphite {

class ResourceCache;
class SharedContext;

#if defined(GRAPHITE_TEST_UTILS)
class Texture;
#endif

/**
 * Base class for objects that can be kept in the ResourceCache.
 */
class Resource {
public:
    Resource(const Resource&) = delete;
    Resource(Resource&&) = delete;
    Resource& operator=(const Resource&) = delete;
    Resource& operator=(Resource&&) = delete;

    // Adds a usage ref to the resource. Named ref so we can easily manage usage refs with sk_sp.
    void ref() const {
        // Only the cache should be able to add the first usage ref to a resource.
        SkASSERT(this->hasUsageRef());
        // No barrier required.
        (void)fUsageRefCnt.fetch_add(+1, std::memory_order_relaxed);
    }

    // Removes a usage ref from the resource
    void unref() const {
        bool shouldFree = false;
        {
            SkAutoMutexExclusive locked(fUnrefMutex);
            SkASSERT(this->hasUsageRef());
            // A release here acts in place of all releases we "should" have been doing in ref().
            if (1 == fUsageRefCnt.fetch_add(-1, std::memory_order_acq_rel)) {
                shouldFree = this->notifyARefIsZero(LastRemovedRef::kUsage);
            }
        }
        if (shouldFree) {
            Resource* mutableThis = const_cast<Resource*>(this);
            mutableThis->internalDispose();
        }
    }

    // Adds a command buffer ref to the resource
    void refCommandBuffer() const {
        // No barrier required.
        (void)fCommandBufferRefCnt.fetch_add(+1, std::memory_order_relaxed);
    }

    // Removes a command buffer ref from the resource
    void unrefCommandBuffer() const {
        bool shouldFree = false;
        {
            SkAutoMutexExclusive locked(fUnrefMutex);
            SkASSERT(this->hasCommandBufferRef());
            // A release here acts in place of all releases we "should" have been doing in ref().
            if (1 == fCommandBufferRefCnt.fetch_add(-1, std::memory_order_acq_rel)) {
                shouldFree = this->notifyARefIsZero(LastRemovedRef::kCommandBuffer);
            }
        }
        if (shouldFree) {
            Resource* mutableThis = const_cast<Resource*>(this);
            mutableThis->internalDispose();
        }
    }

    Ownership ownership() const { return fOwnership; }

    skgpu::Budgeted budgeted() const { return fBudgeted; }

    // Retrieves the amount of GPU memory used by this resource in bytes. It is approximate since we
    // aren't aware of additional padding or copies made by the driver.
    size_t gpuMemorySize() const { return fGpuMemorySize; }

    // Tests whether a object has been abandoned or released. All objects will be in this state
    // after their creating Context is destroyed or abandoned.
    //
    // @return true if the object has been released or abandoned,
    //         false otherwise.
    // TODO: As of now this function isn't really needed because in freeGpuData we are always
    // deleting this object. However, I want to implement all the purging logic first to make sure
    // we don't have a use case for calling internalDispose but not wanting to delete the actual
    // object yet.
    bool wasDestroyed() const { return fSharedContext == nullptr; }

    const GraphiteResourceKey& key() const { return fKey; }
    // This should only ever be called by the ResourceProvider
    void setKey(const GraphiteResourceKey& key) {
        SkASSERT(key.shareable() == Shareable::kNo || this->budgeted() == skgpu::Budgeted::kYes);
        fKey = key;
    }

#if defined(GRAPHITE_TEST_UTILS)
    bool testingShouldDeleteASAP() const { return fDeleteASAP == DeleteASAP::kYes; }

    virtual const Texture* asTexture() const { return nullptr; }
#endif

protected:
    Resource(const SharedContext*, Ownership, skgpu::Budgeted, size_t gpuMemorySize);
    virtual ~Resource();

    const SharedContext* sharedContext() const { return fSharedContext; }

    // Overridden to free GPU resources in the backend API.
    virtual void freeGpuData() = 0;

    // Overridden to call any release callbacks, if necessary
    virtual void invokeReleaseProc() {}

#ifdef SK_DEBUG
    bool debugHasCommandBufferRef() const {
        return hasCommandBufferRef();
    }
#endif

private:
    friend class ProxyCache; // for setDeleteASAP and updateAccessTime

    enum class DeleteASAP : bool {
        kNo = false,
        kYes = true,
    };

    DeleteASAP shouldDeleteASAP() const { return fDeleteASAP; }
    void setDeleteASAP() { fDeleteASAP = DeleteASAP::kYes; }

    // In the ResourceCache this is called whenever a Resource is moved into the purgeableQueue. It
    // may also be called by the ProxyCache to track the time on Resources it is holding on to.
    void updateAccessTime() {
        fLastAccess = skgpu::StdSteadyClock::now();
    }
    skgpu::StdSteadyClock::time_point lastAccessTime() const {
        return fLastAccess;
    }

    ////////////////////////////////////////////////////////////////////////////
    // The following set of functions are only meant to be called by the ResourceCache. We don't
    // want them public general users of a Resource, but they also aren't purely internal calls.
    ////////////////////////////////////////////////////////////////////////////
    friend ResourceCache;

    void makeBudgeted() { fBudgeted = skgpu::Budgeted::kYes; }
    void makeUnbudgeted() { fBudgeted = skgpu::Budgeted::kNo; }

    // This version of ref allows adding a ref when the usage count is 0. This should only be called
    // from the ResourceCache.
    void initialUsageRef() const {
        // Only the cache should be able to add the first usage ref to a resource.
        SkASSERT(fUsageRefCnt >= 0);
        // No barrier required.
        (void)fUsageRefCnt.fetch_add(+1, std::memory_order_relaxed);
    }

    bool isPurgeable() const;
    int* accessReturnIndex()  const { return &fReturnIndex; }
    int* accessCacheIndex()  const { return &fCacheArrayIndex; }

    uint32_t timestamp() const { return fTimestamp; }
    void setTimestamp(uint32_t ts) { fTimestamp = ts; }

    void registerWithCache(sk_sp<ResourceCache>);

    // Adds a cache ref to the resource. This is only called by ResourceCache. A Resource will only
    // ever add a ref when the Resource is part of the cache (i.e. when insertResource is called)
    // and while the Resource is in the ResourceCache::ReturnQueue.
    void refCache() const {
        // No barrier required.
        (void)fCacheRefCnt.fetch_add(+1, std::memory_order_relaxed);
    }

    // Removes a cache ref from the resource. The unref here should only ever be called from the
    // ResourceCache and only in the Recorder thread the ResourceCache is part of.
    void unrefCache() const {
        bool shouldFree = false;
        {
            SkAutoMutexExclusive locked(fUnrefMutex);
            SkASSERT(this->hasCacheRef());
            // A release here acts in place of all releases we "should" have been doing in ref().
            if (1 == fCacheRefCnt.fetch_add(-1, std::memory_order_acq_rel)) {
                shouldFree = this->notifyARefIsZero(LastRemovedRef::kCache);
            }
        }
        if (shouldFree) {
            Resource* mutableThis = const_cast<Resource*>(this);
            mutableThis->internalDispose();
        }
    }

#ifdef SK_DEBUG
    bool isUsableAsScratch() const {
        return fKey.shareable() == Shareable::kNo && !this->hasUsageRef() && fNonShareableInCache;
    }
#endif

    ////////////////////////////////////////////////////////////////////////////
    // The remaining calls are meant to be truely private
    ////////////////////////////////////////////////////////////////////////////
    bool hasUsageRef() const {
        if (0 == fUsageRefCnt.load(std::memory_order_acquire)) {
            // The acquire barrier is only really needed if we return true.  It
            // prevents code conditioned on the result of hasUsageRef() from running until previous
            // owners are all totally done calling unref().
            return false;
        }
        return true;
    }

    bool hasCommandBufferRef() const {
        if (0 == fCommandBufferRefCnt.load(std::memory_order_acquire)) {
            // The acquire barrier is only really needed if we return true.  It
            // prevents code conditioned on the result of hasCommandBufferRef() from running
            // until previous owners are all totally done calling unrefCommandBuffer().
            return false;
        }
        return true;
    }

    bool hasCacheRef() const {
        if (0 == fCacheRefCnt.load(std::memory_order_acquire)) {
            // The acquire barrier is only really needed if we return true. It
            // prevents code conditioned on the result of hasUsageRef() from running until previous
            // owners are all totally done calling unref().
            return false;
        }
        return true;
    }

    bool hasAnyRefs() const {
        return this->hasUsageRef() || this->hasCommandBufferRef() || this->hasCacheRef();
    }

    bool notifyARefIsZero(LastRemovedRef removedRef) const;

    // Frees the object in the underlying 3D API.
    void internalDispose();

    // We need to guard calling unref on the usage and command buffer refs since they each could be
    // unreffed on different threads. This can lead to calling notifyARefIsZero twice with each
    // instance thinking there are no more refs left and both trying to delete the object.
    mutable SkMutex fUnrefMutex;

    SkDEBUGCODE(mutable bool fCalledRemovedFromCache = false;)

    // This is not ref'ed but internalDispose() will be called before the Gpu object is destroyed.
    // That call will set this to nullptr.
    const SharedContext* fSharedContext;

    mutable std::atomic<int32_t> fUsageRefCnt;
    mutable std::atomic<int32_t> fCommandBufferRefCnt;
    mutable std::atomic<int32_t> fCacheRefCnt;

    GraphiteResourceKey fKey;

    sk_sp<ResourceCache> fReturnCache;
    // An index into the return cache so we know whether or not the resource is already waiting to
    // be returned or not.
    mutable int fReturnIndex = -1;

    Ownership fOwnership;

    static const size_t kInvalidGpuMemorySize = ~static_cast<size_t>(0);
    mutable size_t fGpuMemorySize = kInvalidGpuMemorySize;

    // All resource created internally by Graphite and held in the ResourceCache as a shared
    // shared resource or available scratch resource are considered budgeted. Resources that back
    // client owned objects (e.g. SkSurface or SkImage) are not budgeted and do not count against
    // cache limits.
    skgpu::Budgeted fBudgeted;

    // This is only used by ProxyCache::purgeProxiesNotUsedSince which is called from
    // ResourceCache::purgeResourcesNotUsedSince. When kYes, this signals that the Resource
    // should've been purged based on its timestamp at some point regardless of what its
    // current timestamp may indicate (since the timestamp will be updated when the Resource
    // is returned to the ResourceCache).
    DeleteASAP fDeleteASAP = DeleteASAP::kNo;

    // An index into a heap when this resource is purgeable or an array when not. This is maintained
    // by the cache.
    mutable int fCacheArrayIndex = -1;
    // This value reflects how recently this resource was accessed in the cache. This is maintained
    // by the cache.
    uint32_t fTimestamp;
    skgpu::StdSteadyClock::time_point fLastAccess;

    // This is only used during validation checking. Lots of the validation code depends on a
    // resource being purgeable or not. However, purgeable itself just means having no refs. The
    // refs can be removed before a Resource is returned to the cache (or even added to the
    // ReturnQueue).
    SkDEBUGCODE(mutable bool fNonShareableInCache = false);
};

} // namespace skgpu::graphite

#endif // skgpu_graphite_Resource_DEFINED
