/*
 ** Copyright 2022, The Android Open Source Project
 **
 ** Licensed under the Apache License, Version 2.0 (the "License");
 ** you may not use this file except in compliance with the License.
 ** You may obtain a copy of the License at
 **
 **     http://www.apache.org/licenses/LICENSE-2.0
 **
 ** Unless required by applicable law or agreed to in writing, software
 ** distributed under the License is distributed on an "AS IS" BASIS,
 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 ** See the License for the specific language governing permissions and
 ** limitations under the License.
 */

#ifndef ANDROID_MULTIFILE_BLOB_CACHE_H
#define ANDROID_MULTIFILE_BLOB_CACHE_H

#include <EGL/egl.h>
#include <EGL/eglext.h>

#include <android-base/thread_annotations.h>
#include <cutils/properties.h>
#include <future>
#include <map>
#include <queue>
#include <string>
#include <thread>
#include <unordered_map>
#include <unordered_set>

#include "FileBlobCache.h"

namespace android {

constexpr uint32_t kMultifileBlobCacheVersion = 2;
constexpr char kMultifileBlobCacheStatusFile[] = "cache.status";

struct MultifileHeader {
    uint32_t magic;
    uint32_t crc;
    EGLsizeiANDROID keySize;
    EGLsizeiANDROID valueSize;
};

struct MultifileEntryStats {
    EGLsizeiANDROID valueSize;
    size_t fileSize;
    time_t accessTime;
};

struct MultifileStatus {
    uint32_t magic;
    uint32_t crc;
    uint32_t cacheVersion;
    char buildId[PROP_VALUE_MAX];
};

struct MultifileHotCache {
    int entryFd;
    uint8_t* entryBuffer;
    size_t entrySize;
};

enum class TaskCommand {
    Invalid = 0,
    WriteToDisk,
    Exit,
};

class DeferredTask {
public:
    DeferredTask(TaskCommand command)
          : mCommand(command), mEntryHash(0), mBuffer(nullptr), mBufferSize(0) {}

    TaskCommand getTaskCommand() { return mCommand; }

    void initWriteToDisk(uint32_t entryHash, std::string fullPath, uint8_t* buffer,
                         size_t bufferSize) {
        mCommand = TaskCommand::WriteToDisk;
        mEntryHash = entryHash;
        mFullPath = std::move(fullPath);
        mBuffer = buffer;
        mBufferSize = bufferSize;
    }

    uint32_t getEntryHash() { return mEntryHash; }
    std::string& getFullPath() { return mFullPath; }
    uint8_t* getBuffer() { return mBuffer; }
    size_t getBufferSize() { return mBufferSize; };

private:
    TaskCommand mCommand;

    // Parameters for WriteToDisk
    uint32_t mEntryHash;
    std::string mFullPath;
    uint8_t* mBuffer;
    size_t mBufferSize;
};

class MultifileBlobCache {
public:
    MultifileBlobCache(size_t maxKeySize, size_t maxValueSize, size_t maxTotalSize,
                       size_t maxTotalEntries, const std::string& baseDir);
    ~MultifileBlobCache();

    void set(const void* key, EGLsizeiANDROID keySize, const void* value,
             EGLsizeiANDROID valueSize);
    EGLsizeiANDROID get(const void* key, EGLsizeiANDROID keySize, void* value,
                        EGLsizeiANDROID valueSize);

    void finish();

    size_t getTotalSize() const { return mTotalCacheSize; }
    size_t getTotalEntries() const { return mTotalCacheEntries; }

    const std::string& getCurrentBuildId() const { return mBuildId; }
    void setCurrentBuildId(const std::string& buildId) { mBuildId = buildId; }

    uint32_t getCurrentCacheVersion() const { return mCacheVersion; }
    void setCurrentCacheVersion(uint32_t cacheVersion) { mCacheVersion = cacheVersion; }

private:
    void trackEntry(uint32_t entryHash, EGLsizeiANDROID valueSize, size_t fileSize,
                    time_t accessTime);
    bool contains(uint32_t entryHash) const;
    bool removeEntry(uint32_t entryHash);
    MultifileEntryStats getEntryStats(uint32_t entryHash);

    bool createStatus(const std::string& baseDir);
    bool checkStatus(const std::string& baseDir);

    size_t getFileSize(uint32_t entryHash);
    size_t getValueSize(uint32_t entryHash);

    void increaseTotalCacheSize(size_t fileSize);
    void decreaseTotalCacheSize(size_t fileSize);

    bool addToHotCache(uint32_t entryHash, int fd, uint8_t* entryBufer, size_t entrySize);
    bool removeFromHotCache(uint32_t entryHash);

    bool clearCache();
    void trimCache();
    bool applyLRU(size_t cacheSizeLimit, size_t cacheEntryLimit);

    bool mInitialized;
    std::string mMultifileDirName;

    std::string mBuildId;
    uint32_t mCacheVersion;

    std::unordered_set<uint32_t> mEntries;
    std::unordered_map<uint32_t, MultifileEntryStats> mEntryStats;
    std::unordered_map<uint32_t, MultifileHotCache> mHotCache;

    size_t mMaxKeySize;
    size_t mMaxValueSize;
    size_t mMaxTotalSize;
    size_t mMaxTotalEntries;
    size_t mTotalCacheSize;
    size_t mTotalCacheEntries;
    size_t mHotCacheLimit;
    size_t mHotCacheEntryLimit;
    size_t mHotCacheSize;

    // Below are the components used for deferred writes

    // Track whether we have pending writes for an entry
    std::mutex mDeferredWriteStatusMutex;
    std::multimap<uint32_t, uint8_t*> mDeferredWrites GUARDED_BY(mDeferredWriteStatusMutex);

    // Functions to work through tasks in the queue
    void processTasks();
    void processTasksImpl(bool* exitThread);
    void processTask(DeferredTask& task);

    // Used by main thread to create work for the worker thread
    void queueTask(DeferredTask&& task);

    // Used by main thread to wait for worker thread to complete all outstanding work.
    void waitForWorkComplete();

    std::thread mTaskThread;
    std::queue<DeferredTask> mTasks;
    std::mutex mWorkerMutex;

    // This condition will block the worker thread until a task is queued
    std::condition_variable mWorkAvailableCondition;

    // This condition will block the main thread while the worker thread still has tasks
    std::condition_variable mWorkerIdleCondition;

    // This bool will track whether all tasks have been completed
    bool mWorkerThreadIdle;
};

}; // namespace android

#endif // ANDROID_MULTIFILE_BLOB_CACHE_H
