//
// Copyright 2018 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// BlobCache: Stores keyed blobs in memory to support EGL_ANDROID_blob_cache.
// Can be used in conjunction with the platform layer to warm up the cache from
// disk.  MemoryProgramCache uses this to handle caching of compiled programs.

#include "libANGLE/BlobCache.h"
#include "common/utilities.h"
#include "libANGLE/Context.h"
#include "libANGLE/Display.h"
#include "libANGLE/histogram_macros.h"
#include "platform/PlatformMethods.h"

#define USE_SYSTEM_ZLIB
#include "compression_utils_portable.h"

namespace egl
{

namespace
{
enum CacheResult
{
    kCacheMiss,
    kCacheHitMemory,
    kCacheHitDisk,
    kCacheResultMax,
};

}  // anonymous namespace

// In oder to store more cache in blob cache, compress cacheData to compressedData
// before being stored.
bool CompressBlobCacheData(const size_t cacheSize,
                           const uint8_t *cacheData,
                           angle::MemoryBuffer *compressedData)
{
    uLong uncompressedSize       = static_cast<uLong>(cacheSize);
    uLong expectedCompressedSize = zlib_internal::GzipExpectedCompressedSize(uncompressedSize);

    // Allocate memory.
    if (!compressedData->resize(expectedCompressedSize))
    {
        ERR() << "Failed to allocate memory for compression";
        return false;
    }

    int zResult = zlib_internal::GzipCompressHelper(compressedData->data(), &expectedCompressedSize,
                                                    cacheData, uncompressedSize, nullptr, nullptr);

    if (zResult != Z_OK)
    {
        ERR() << "Failed to compress cache data: " << zResult;
        return false;
    }

    // Resize it to expected size.
    if (!compressedData->resize(expectedCompressedSize))
    {
        return false;
    }

    return true;
}

bool DecompressBlobCacheData(const uint8_t *compressedData,
                             const size_t compressedSize,
                             angle::MemoryBuffer *uncompressedData)
{
    // Call zlib function to decompress.
    uint32_t uncompressedSize =
        zlib_internal::GetGzipUncompressedSize(compressedData, compressedSize);

    // Allocate enough memory.
    if (!uncompressedData->resize(uncompressedSize))
    {
        ERR() << "Failed to allocate memory for decompression";
        return false;
    }

    uLong destLen = uncompressedSize;
    int zResult   = zlib_internal::GzipUncompressHelper(
          uncompressedData->data(), &destLen, compressedData, static_cast<uLong>(compressedSize));

    if (zResult != Z_OK)
    {
        ERR() << "Failed to decompress data: " << zResult << "\n";
        return false;
    }

    // Resize it to expected size.
    if (!uncompressedData->resize(destLen))
    {
        return false;
    }

    return true;
}

BlobCache::BlobCache(size_t maxCacheSizeBytes)
    : mBlobCache(maxCacheSizeBytes), mSetBlobFunc(nullptr), mGetBlobFunc(nullptr)
{}

BlobCache::~BlobCache() {}

void BlobCache::put(const BlobCache::Key &key, angle::MemoryBuffer &&value)
{
    if (areBlobCacheFuncsSet())
    {
        std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
        // Store the result in the application's cache
        mSetBlobFunc(key.data(), key.size(), value.data(), value.size());
    }
    else
    {
        populate(key, std::move(value), CacheSource::Memory);
    }
}

bool BlobCache::compressAndPut(const BlobCache::Key &key,
                               angle::MemoryBuffer &&uncompressedValue,
                               size_t *compressedSize)
{
    angle::MemoryBuffer compressedValue;
    if (!CompressBlobCacheData(uncompressedValue.size(), uncompressedValue.data(),
                               &compressedValue))
    {
        return false;
    }
    if (compressedSize != nullptr)
        *compressedSize = compressedValue.size();
    put(key, std::move(compressedValue));
    return true;
}

void BlobCache::putApplication(const BlobCache::Key &key, const angle::MemoryBuffer &value)
{
    if (areBlobCacheFuncsSet())
    {
        std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
        mSetBlobFunc(key.data(), key.size(), value.data(), value.size());
    }
}

void BlobCache::populate(const BlobCache::Key &key, angle::MemoryBuffer &&value, CacheSource source)
{
    std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
    CacheEntry newEntry;
    newEntry.first  = std::move(value);
    newEntry.second = source;

    // Cache it inside blob cache only if caching inside the application is not possible.
    mBlobCache.put(key, std::move(newEntry), newEntry.first.size());
}

bool BlobCache::get(angle::ScratchBuffer *scratchBuffer,
                    const BlobCache::Key &key,
                    BlobCache::Value *valueOut,
                    size_t *bufferSizeOut)
{
    // Look into the application's cache, if there is such a cache
    if (areBlobCacheFuncsSet())
    {
        std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
        EGLsizeiANDROID valueSize = mGetBlobFunc(key.data(), key.size(), nullptr, 0);
        if (valueSize <= 0)
        {
            return false;
        }

        angle::MemoryBuffer *scratchMemory;
        bool result = scratchBuffer->get(valueSize, &scratchMemory);
        if (!result)
        {
            ERR() << "Failed to allocate memory for binary blob";
            return false;
        }

        EGLsizeiANDROID originalValueSize = valueSize;
        valueSize = mGetBlobFunc(key.data(), key.size(), scratchMemory->data(), valueSize);

        // Make sure the key/value pair still exists/is unchanged after the second call
        // (modifications to the application cache by another thread are a possibility)
        if (valueSize != originalValueSize)
        {
            // This warning serves to find issues with the application cache, none of which are
            // currently known to be thread-safe.  If such a use ever arises, this WARN can be
            // removed.
            WARN() << "Binary blob no longer available in cache (removed by a thread?)";
            return false;
        }

        *valueOut      = BlobCache::Value(scratchMemory->data(), scratchMemory->size());
        *bufferSizeOut = valueSize;
        return true;
    }

    std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
    // Otherwise we are doing caching internally, so try to find it there
    const CacheEntry *entry;
    bool result = mBlobCache.get(key, &entry);

    if (result)
    {

        *valueOut      = BlobCache::Value(entry->first.data(), entry->first.size());
        *bufferSizeOut = entry->first.size();
    }

    return result;
}

bool BlobCache::getAt(size_t index, const BlobCache::Key **keyOut, BlobCache::Value *valueOut)
{
    std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
    const CacheEntry *valueBuf;
    bool result = mBlobCache.getAt(index, keyOut, &valueBuf);
    if (result)
    {
        *valueOut = BlobCache::Value(valueBuf->first.data(), valueBuf->first.size());
    }
    return result;
}

BlobCache::GetAndDecompressResult BlobCache::getAndDecompress(
    angle::ScratchBuffer *scratchBuffer,
    const BlobCache::Key &key,
    angle::MemoryBuffer *uncompressedValueOut)
{
    ASSERT(uncompressedValueOut);

    Value compressedValue;
    size_t compressedSize;
    if (!get(scratchBuffer, key, &compressedValue, &compressedSize))
    {
        return GetAndDecompressResult::NotFound;
    }

    {
        // This needs to be locked because `DecompressBlobCacheData` is reading shared memory from
        // `compressedValue.data()`.
        std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
        if (!DecompressBlobCacheData(compressedValue.data(), compressedSize, uncompressedValueOut))
        {
            return GetAndDecompressResult::DecompressFailure;
        }
    }

    return GetAndDecompressResult::GetSuccess;
}

void BlobCache::remove(const BlobCache::Key &key)
{
    std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
    mBlobCache.eraseByKey(key);
}

void BlobCache::setBlobCacheFuncs(EGLSetBlobFuncANDROID set, EGLGetBlobFuncANDROID get)
{
    std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
    mSetBlobFunc = set;
    mGetBlobFunc = get;
}

bool BlobCache::areBlobCacheFuncsSet() const
{
    std::scoped_lock<std::mutex> lock(mBlobCacheMutex);
    // Either none or both of the callbacks should be set.
    ASSERT((mSetBlobFunc != nullptr) == (mGetBlobFunc != nullptr));

    return mSetBlobFunc != nullptr && mGetBlobFunc != nullptr;
}

}  // namespace egl
