/*
 * Copyright 2021 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_PipelineDataCache_DEFINED
#define skgpu_graphite_PipelineDataCache_DEFINED

#include "src/base/SkArenaAlloc.h"
#include "src/core/SkTHash.h"
#include "src/gpu/graphite/PipelineData.h"


namespace skgpu::graphite {

// Add a block of data to the cache and return a stable pointer to the contents (assuming that a
// resettable gatherer had accumulated the input data pointer).
//
// If an identical block of data is already in the cache, that existing pointer is returned, making
// pointer comparison suitable when comparing data blocks retrieved from the cache.
//
// T must define a hash() function, an operator==, and a static Make(const T&, SkArenaAlloc*)
// factory that's used to copy the data into an arena allocation owned by the PipelineDataCache.
template<typename T>
class PipelineDataCache {
public:
    PipelineDataCache() = default;

    const T* insert(const T& dataBlock) {
        DataRef data{&dataBlock}; // will not be persisted, since pointer isn't from the arena.
        const DataRef* existing = fDataPointers.find(data);
        if (existing) {
            return existing->fPointer;
        } else {
            // Need to make a copy of dataBlock into the arena
            T* copy = T::Make(dataBlock, &fArena);
            fDataPointers.add(DataRef{copy});
            return copy;
        }
    }

    // The number of unique T objects in the cache
    int count() const {
        return fDataPointers.count();
    }

    // Call fn on every item in the set.  You may not mutate anything.
    template <typename Fn>  // f(T), f(const T&)
    void foreach(Fn&& fn) const {
        fDataPointers.foreach([fn](const DataRef& ref){
            fn(ref.fPointer);
        });
    }

private:
    struct DataRef {
        const T* fPointer;

        bool operator==(const DataRef& o) const {
            if (!fPointer || !o.fPointer) {
                return !fPointer && !o.fPointer;
            } else {
                return *fPointer == *o.fPointer;
            }
        }
    };
    struct Hash {
        // This hash operator de-references and hashes the data contents
        size_t operator()(const DataRef& dataBlock) const {
            return dataBlock.fPointer ? dataBlock.fPointer->hash() : 0;
        }
    };

    skia_private::THashSet<DataRef, Hash> fDataPointers;
    // Holds the data that is pointed to by fDataPointers
    SkArenaAlloc fArena{0};
};

// A UniformDataCache only lives for a single Recording. It's used to deduplicate uniform data
// blocks uploaded to uniform/storage buffers for a DrawPass pipeline.
using UniformDataCache = PipelineDataCache<UniformDataBlock>;

// A TextureDataCache only lives for a single Recording. When a Recording is snapped it is pulled
// off of the Recorder and goes with the Recording as a record of the required Textures and
// Samplers.
using TextureDataCache = PipelineDataCache<TextureDataBlock>;

} // namespace skgpu::graphite

#endif // skgpu_graphite_PipelineDataCache_DEFINED
