#include "./storageprivate.h"

#include <c++utilities/conversion/stringbuilder.h>
#include <c++utilities/io/ansiescapecodes.h>

#include <iostream>

using namespace CppUtilities;

namespace LibPkg {

template <typename StorageEntryType>
template <typename IndexType>
auto StorageCacheEntries<StorageEntryType>::find(const IndexType &ref) -> StorageEntry *
{
    const auto &index = m_entries.template get<IndexType>();
    if (auto i = index.find(ref); i != index.end()) {
        m_entries.relocate(m_entries.begin(), m_entries.template project<0>(i));
        return &i.get_node()->value();
    }
    return nullptr;
}

template <typename StorageEntryType> auto StorageCacheEntries<StorageEntryType>::insert(StorageEntry &&entry) -> StorageEntry &
{
    const auto [i, newItem] = m_entries.emplace_front(entry);
    if (!newItem) {
        m_entries.relocate(m_entries.begin(), i);
    } else if (m_entries.size() > m_limit) {
        m_entries.pop_back();
    }
    return i.get_node()->value();
}

template <typename StorageEntryType> std::size_t StorageCacheEntries<StorageEntryType>::clear(const Storage &storage)
{
    auto count = std::size_t();
    for (auto i = m_entries.begin(); i != m_entries.end();) {
        if (i->ref.relatedStorage == &storage) {
            i = m_entries.erase(i);
            ++count;
        } else {
            ++i;
        }
    }
    return count;
}

template <typename StorageEntryType> void StorageCacheEntries<StorageEntryType>::setLimit(std::size_t limit)
{
    m_limit = limit;
    while (m_entries.size() > limit) {
        m_entries.pop_back();
    }
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
auto StorageCache<StorageEntriesType, StorageType, SpecType>::retrieve(Storage &storage, ROTxn *txn, StorageID storageID) -> SpecType
{
    // check for package in cache, should be ok even if the db is being updated
    const auto ref = typename StorageEntryByID<typename Entries::StorageEntry>::result_type{ storageID, &storage };
    auto lock = std::unique_lock(m_mutex);
    if (auto *const existingCacheEntry = m_entries.find(ref)) {
        return SpecType(existingCacheEntry->id, existingCacheEntry->entry);
    }
    // check for package in storage, populate cache entry
    lock.unlock();
    auto entry = std::make_shared<Entry>();
    if (auto id = txn ? txn->get(storageID, *entry) : storage.packages.getROTransaction().get(storageID, *entry)) {
        // try to acquire update lock to avoid update existing cache entries while db is being updated
        if (const auto updateLock = std::unique_lock(storage.updateMutex, std::try_to_lock)) {
            using CacheEntry = typename Entries::StorageEntry;
            using CacheRef = typename Entries::Ref;
            auto newCacheEntry = CacheEntry(CacheRef(storage, entry), id);
            newCacheEntry.entry = entry;
            lock = std::unique_lock(m_mutex);
            m_entries.insert(std::move(newCacheEntry));
            lock.unlock();
        }
        return SpecType(id, entry);
    }
    return SpecType(0, std::shared_ptr<Entry>());
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
auto StorageCache<StorageEntriesType, StorageType, SpecType>::retrieve(Storage &storage, StorageID storageID) -> SpecType
{
    return retrieve(storage, nullptr, storageID);
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
auto StorageCache<StorageEntriesType, StorageType, SpecType>::retrieve(Storage &storage, RWTxn *txn, const std::string &entryName) -> SpecType
{
    // do not attempt to fetch empty entries (apparently can lead to "Getting data: MDB_BAD_VALSIZE: Unsupported size of key/DB name/data, or wrong DUPFIXED size")
    if (entryName.empty()) {
        return SpecType(0, std::shared_ptr<Entry>());
    }
    // check for package in cache, should be ok even if the db is being updated
    using CacheRef = typename Entries::Ref;
    const auto ref = CacheRef(storage, entryName);
    auto lock = std::unique_lock(m_mutex);
    if (auto *const existingCacheEntry = m_entries.find(ref)) {
        return SpecType(existingCacheEntry->id, existingCacheEntry->entry);
    }
    lock.unlock();
    // check for package in storage, populate cache entry
    auto entry = std::make_shared<Entry>();
    if (auto id = txn ? txn->template get<0>(entryName, *entry) : storage.packages.getROTransaction().template get<0>(entryName, *entry)) {
        // try to acquire update lock to avoid update existing cache entries while db is being updated
        if (const auto updateLock = std::unique_lock(storage.updateMutex, std::try_to_lock)) {
            using CacheEntry = typename Entries::StorageEntry;
            auto newCacheEntry = CacheEntry(CacheRef(storage, entry), id);
            newCacheEntry.entry = entry;
            lock = std::unique_lock(m_mutex);
            m_entries.insert(std::move(newCacheEntry));
            lock.unlock();
        }
        return SpecType(id, entry);
    }
    return SpecType(0, std::shared_ptr<Entry>());
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
auto StorageCache<StorageEntriesType, StorageType, SpecType>::retrieve(Storage &storage, const std::string &entryName) -> SpecType
{
    return retrieve(storage, nullptr, entryName);
}

/*!
 * \brief Stores the specified \a entry.
 * \remarks The entry may exist or may not exist. A lookup for an existing entry is done to take over
 *          deps/provides from the existing entry if it makes sense.
 */
template <typename StorageEntriesType, typename StorageType, typename SpecType>
auto StorageCache<StorageEntriesType, StorageType, SpecType>::store(Storage &storage, RWTxn &txn, const std::shared_ptr<Entry> &entry) -> StoreResult
{
    // check for package in cache
    using CacheEntry = typename Entries::StorageEntry;
    using CacheRef = typename Entries::Ref;
    auto res = StorageCache::StoreResult();
    if (entry->name.empty()) {
        return res;
    }
    const auto ref = CacheRef(storage, entry);
    auto lock = std::unique_lock(m_mutex);
    auto *cacheEntry = m_entries.find(ref);
    if (cacheEntry) {
        // retain certain information obtained from package contents if this is actually the same package as before
        res.id = cacheEntry->id;
        res.oldEntry = cacheEntry->entry;
        entry->addDepsAndProvidesFromOtherPackage(*res.oldEntry);
    }
    lock.unlock();

    // check for package in storage
    if (!res.oldEntry) {
        res.oldEntry = std::make_shared<Entry>();
        if ((res.id = txn.template get<0>(entry->name, *res.oldEntry))) {
            entry->addDepsAndProvidesFromOtherPackage(*res.oldEntry);
        } else {
            res.oldEntry.reset();
        }
    }

    // update package in storage
    res.id = txn.put(*entry, res.id);

    // update cache entry
    lock = std::unique_lock(m_mutex);
    if (cacheEntry) {
        cacheEntry->ref.entryName = &entry->name;
    } else {
        cacheEntry = &m_entries.insert(CacheEntry(ref, res.id));
    }
    cacheEntry->entry = entry;
    lock.unlock();

    res.updated = true;
    return res;
}

/*!
 * \brief Stores the specified \a entry with the specified \a storageID.
 * \remarks This is used to update an existing entry with a known ID.
 */
template <typename StorageEntriesType, typename StorageType, typename SpecType>
auto StorageCache<StorageEntriesType, StorageType, SpecType>::store(
    Storage &storage, RWTxn &txn, StorageID storageID, const std::shared_ptr<Entry> &entry) -> void
{
    // update package in storage
    const auto id = txn.put(*entry, storageID);

    // update cache entry
    using CacheEntry = typename Entries::StorageEntry;
    using CacheRef = typename Entries::Ref;
    const auto ref = CacheRef(storage, entry);
    const auto lock = std::unique_lock(m_mutex);
    const auto cacheEntry = &m_entries.insert(CacheEntry(ref, id));
    cacheEntry->entry = entry;
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
bool StorageCache<StorageEntriesType, StorageType, SpecType>::invalidate(Storage &storage, const std::string &entryName)
{
    // remove package from cache
    invalidateCacheOnly(storage, entryName);
    // remove package from storage
    auto txn = storage.packages.getRWTransaction();
    if (auto i = txn.template find<0>(entryName); i != txn.end()) {
        i.del();
        txn.commit();
        return true;
    }
    return false;
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
bool StorageCache<StorageEntriesType, StorageType, SpecType>::invalidateCacheOnly(Storage &storage, const std::string &entryName)
{
    const auto ref = typename Entries::Ref(storage, entryName);
    const auto lock = std::unique_lock(m_mutex);
    m_entries.erase(ref);
    return true;
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
void StorageCache<StorageEntriesType, StorageType, SpecType>::clear(Storage &storage)
{
    clearCacheOnly(storage);
    auto packagesTxn = storage.packages.getRWTransaction();
    auto txnHandle = packagesTxn.getTransactionHandle();
    packagesTxn.clear();
    auto providedDepsTxn = storage.providedDeps.getRWTransaction(txnHandle);
    providedDepsTxn.clear();
    auto requiredDepsTxn = storage.requiredDeps.getRWTransaction(txnHandle);
    requiredDepsTxn.clear();
    auto providedLibsTxn = storage.providedLibs.getRWTransaction(txnHandle);
    providedLibsTxn.clear();
    auto requiredLibsTxn = storage.requiredLibs.getRWTransaction(txnHandle);
    requiredLibsTxn.clear();
    packagesTxn.commit();
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
void StorageCache<StorageEntriesType, StorageType, SpecType>::clearCacheOnly(Storage &storage)
{
    const auto lock = std::unique_lock(m_mutex);
    m_entries.clear(storage);
}

template <typename StorageEntriesType, typename StorageType, typename SpecType>
void StorageCache<StorageEntriesType, StorageType, SpecType>::setLimit(std::size_t limit)
{
    const auto lock = std::unique_lock(m_mutex);
    m_entries.setLimit(limit);
}

template struct StorageCacheRef<DatabaseStorage, Package>;
template struct StorageCacheEntry<PackageCacheRef, Package>;
template class StorageCacheEntries<PackageCacheEntry>;
template struct StorageCache<PackageCacheEntries, PackageStorage, PackageSpec>;

StorageDistribution::StorageDistribution(const char *path, std::uint32_t maxDbs)
{
    m_env = LMDBSafe::getMDBEnv(path, MDB_NOSUBDIR, 0600, maxDbs);
}

DatabaseStorage::DatabaseStorage(const std::shared_ptr<LMDBSafe::MDBEnv> &env, PackageCache &packageCache, std::string_view uniqueDatabaseName)
    : packageCache(packageCache)
    , packages(env, argsToString(uniqueDatabaseName, "_packages"))
    , providedDeps(env, argsToString(uniqueDatabaseName, "_provides"))
    , requiredDeps(env, argsToString(uniqueDatabaseName, "_requires"))
    , providedLibs(env, argsToString(uniqueDatabaseName, "_libprovides"))
    , requiredLibs(env, argsToString(uniqueDatabaseName, "_librequires"))
    , m_env(env)
{
    std::cout << EscapeCodes::Phrases::InfoMessage << "Initialized database storage for \"" << uniqueDatabaseName << "\"\n";
}

std::size_t hash_value(const PackageCacheRef &ref)
{
    const auto hasher1 = boost::hash<const LibPkg::DatabaseStorage *>();
    const auto hasher2 = boost::hash<std::string>();
    return ((hasher1(ref.relatedStorage) ^ (hasher2(*ref.entryName) << 1)) >> 1);
}

std::size_t hash_value(const PackageCacheEntryByID &entryByID)
{
    const auto hasher1 = boost::hash<StorageID>();
    const auto hasher2 = boost::hash<const LibPkg::DatabaseStorage *>();
    return ((hasher1(entryByID.id) ^ (hasher2(entryByID.storage) << 1)) >> 1);
}

} // namespace LibPkg
