#ifndef LIBPKG_DATA_DATABASE_H
#define LIBPKG_DATA_DATABASE_H

#include "./package.h"
#include "./siglevel.h"
#include "./storagefwd.h"

#include "../global.h"

#include <c++utilities/chrono/datetime.h>
#include <c++utilities/misc/flagenumclass.h>

#include <atomic>
#include <filesystem>
#include <optional>
#include <regex>
#include <unordered_set>

namespace LibPkg {

struct Config;
struct Database;

struct LIBPKG_EXPORT DatabaseInfo {
    std::string name;
    std::string arch;
};

struct LIBPKG_EXPORT PackageSearchResult {
    PackageSearchResult();
    PackageSearchResult(Database &database, const std::shared_ptr<Package> &package, StorageID id);
    PackageSearchResult(Database &database, std::shared_ptr<Package> &&package, StorageID id);
    PackageSearchResult(Database &database, Package &&package, StorageID id);
    bool operator==(const PackageSearchResult &other) const;

    /// \brief The related database.
    /// \remarks
    /// - The find functions always uses Database* and it is guaranteed to be never nullptr.
    /// - The deserialization functions always use DatabaseInfo and the values might be empty if the source was empty.
    /// - The serialization functions can cope with both alternatives.
    std::variant<Database *, DatabaseInfo> db;
    std::shared_ptr<Package> pkg;
    StorageID id;
};

struct LIBPKG_EXPORT PackageBaseSearchResult {
    PackageBaseSearchResult() = default;
    PackageBaseSearchResult(const Database &database, const PackageBase &package, StorageID id);

    /// \brief The related database.
    /// \remarks
    /// - The find functions always uses Database* and it is guaranteed to be never nullptr.
    /// - The deserialization functions always use DatabaseInfo and the values might be empty if the source was empty.
    /// - The serialization functions can cope with both alternatives.
    const Database *db = nullptr;
    const PackageBase *pkg = nullptr;
    StorageID id = 0;
};

/*!
 * \brief The DatabaseUsage enum specifies the usage of a database within pacman.
 */
enum class DatabaseUsage {
    None = 0,
    Sync = (1 << 0), /*! The database is used when synchronizing. */
    Search = (1 << 1), /*! The database is used when searching. */
    Install = (1 << 2), /*! The database is used to install packages. */
    Upgrade = (1 << 3), /*! The database is used to upgrade packages. */
    All = (1 << 4) - 1, /*! The database is used for everything. */
};

enum class UpdateCheckOptions {
    None = 0,
    ConsiderRegularPackage = (1 << 0),
};

} // namespace LibPkg

CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(LibPkg, LibPkg::DatabaseUsage)
CPP_UTILITIES_MARK_FLAG_ENUM_CLASS(LibPkg, LibPkg::UpdateCheckOptions)

namespace LibPkg {

struct LIBPKG_EXPORT PackageUpdate : public ReflectiveRapidJSON::JsonSerializable<PackageUpdate>,
                                     public ReflectiveRapidJSON::BinarySerializable<PackageUpdate> {
    PackageUpdate(PackageSearchResult &&oldVersion = PackageSearchResult(), PackageSearchResult &&newVersion = PackageSearchResult())
        : oldVersion(oldVersion)
        , newVersion(newVersion)
    {
    }
    PackageSearchResult oldVersion;
    PackageSearchResult newVersion;
};

struct LIBPKG_EXPORT PackageUpdates : public ReflectiveRapidJSON::JsonSerializable<PackageUpdates>,
                                      public ReflectiveRapidJSON::BinarySerializable<PackageUpdates, 1> {
    std::vector<PackageUpdate> versionUpdates;
    std::vector<PackageUpdate> packageUpdates;
    std::vector<PackageUpdate> downgrades;
    std::vector<PackageSearchResult> orphans;
};

struct LIBPKG_EXPORT PackageLocation {
    std::filesystem::path pathWithinRepo;
    std::filesystem::path storageLocation;
    std::optional<std::filesystem::filesystem_error> error;
    bool exists = false;
};

struct LIBPKG_EXPORT UnresolvedDependencies : public ReflectiveRapidJSON::JsonSerializable<UnresolvedDependencies>,
                                              public ReflectiveRapidJSON::BinarySerializable<UnresolvedDependencies, 1> {
    std::vector<Dependency> deps;
    std::vector<std::string> libs;
};

struct PackageUpdaterPrivate;

struct LIBPKG_EXPORT PackageUpdater {
    explicit PackageUpdater(Database &database, bool clear = false);
    ~PackageUpdater();

    PackageSpec findPackageWithID(const std::string &packageName);
    void beginUpdate(StorageID packageID, const std::shared_ptr<Package> &package);
    void endUpdate(StorageID packageID, const std::shared_ptr<Package> &package);
    StorageID update(const std::shared_ptr<Package> &package);
    const std::unordered_set<StorageID> &handledIDs() const;
    std::size_t packageCount() const;
    bool insertFromDatabaseFile(const std::string &databaseFilePath);
    void commit();

private:
    Database &m_database;
    std::unique_ptr<PackageUpdaterPrivate> m_d;
};

struct AtomicDateTime : public std::atomic<CppUtilities::DateTime> {
    AtomicDateTime(CppUtilities::DateTime value = CppUtilities::DateTime())
        : std::atomic<CppUtilities::DateTime>(value)
    {
    }
    AtomicDateTime(AtomicDateTime &&other)
        : std::atomic<CppUtilities::DateTime>(other.load())
    {
    }
    AtomicDateTime &operator=(AtomicDateTime &&other)
    {
        store(other.load());
        return *this;
    }
};

struct LIBPKG_EXPORT Database : public ReflectiveRapidJSON::JsonSerializable<Database>, public ReflectiveRapidJSON::BinarySerializable<Database> {
    using PackageVisitorBase = std::function<bool(StorageID, std::shared_ptr<PackageBase> &&)>; // package is invalidated/reused unless moved from!!!
    using PackageVisitorMove = std::function<bool(StorageID, std::shared_ptr<Package> &&)>; // package is invalidated/reused unless moved from!!!
    using PackageVisitorConst = std::function<bool(StorageID, const std::shared_ptr<Package> &)>;
    using PackageVisitorByName = std::function<bool(std::string_view, const std::function<PackageSpec(void)> &)>;
    using PackageVisitorByNameBase = std::function<bool(std::string_view, const std::function<StorageID(PackageBase &)> &)>;

    friend struct PackageUpdater;

    explicit Database(const std::string &name = std::string(), const std::string &path = std::string());
    explicit Database(std::string &&name, std::string &&path);
    Database(Database &&other);
    ~Database();
    Database &operator=(Database &&rhs) = default;

    void initStorage(StorageDistribution &storage);
    void rebuildDb();
    void dumpDb(const std::optional<std::regex> &filterRegex);
    void deducePathsFromLocalDirs();
    void resetConfiguration(bool keepLocalPaths = false);
    void clearPackages();
    void loadPackagesFromConfiguredPaths(bool withFiles = false, bool force = false);
    void loadPackages(const std::string &databaseFilePath, CppUtilities::DateTime lastModified);
    static bool isFileRelevant(const char *filePath, const char *fileName, mode_t);
    std::vector<std::shared_ptr<Package>> findPackages(const std::function<bool(const Database &, const Package &)> &pred);
    void allPackages(const PackageVisitorMove &visitor);
    void allPackagesBase(const PackageVisitorBase &visitor);
    void allPackagesByName(const PackageVisitorByName &visitor);
    void allPackagesByName(const PackageVisitorByNameBase &visitor);
    std::size_t packageCount() const;
    void providingPackages(const Dependency &dependency, bool reverse, const PackageVisitorConst &visitor);
    void providingPackagesBase(const Dependency &dependency, bool reverse, const PackageVisitorBase &visitor);
    void providingPackages(const std::string &libraryName, bool reverse, const PackageVisitorConst &visitor);
    void providingPackagesBase(const std::string &libraryName, bool reverse, const PackageVisitorBase &visitor);
    bool provides(const Dependency &dependency, bool reverse = false) const;
    bool provides(const std::string &libraryName, bool reverse = false) const;
    std::shared_ptr<Package> findPackage(StorageID packageID);
    std::shared_ptr<Package> findPackage(const std::string &packageName);
    PackageSpec findPackageWithID(const std::string &packageName);
    StorageID findBasePackageWithID(const std::string &packageName, PackageBase &basePackage);
    void removePackage(const std::string &packageName);
    StorageID updatePackage(const std::shared_ptr<Package> &package);
    std::unordered_map<PackageSpec, UnresolvedDependencies> detectUnresolvedPackages(Config &config,
        const std::vector<std::shared_ptr<Package>> &newPackages, const DependencySet &removedPackages,
        const std::unordered_set<std::string_view> &depsToIgnore = std::unordered_set<std::string_view>(),
        const std::unordered_set<std::string_view> &libsToIgnore = std::unordered_set<std::string_view>());
    PackageUpdates checkForUpdates(const std::vector<Database *> &updateSources, UpdateCheckOptions options = UpdateCheckOptions::None);
    PackageLocation locatePackage(const std::string &packageName) const;
    std::string filesPathFromRegularPath() const;
    static bool isStaging(std::string_view name);
    static bool isTesting(std::string_view name);
    static bool isDebug(std::string_view name);
    static std::string_view specialSuffix(std::string_view name);
    bool isStaging() const;
    bool isTesting() const;
    bool isDebug() const;
    std::string_view nameWithoutDebugSuffix() const;
    std::pair<std::string_view, std::string_view> baseNameAndSuffix() const;
    std::string stagingName() const;
    std::string protectedName() const;
    std::string debugName() const;

public:
    std::string name;
    std::string path;
    std::string filesPath;
    std::vector<std::string> mirrors;
    DatabaseUsage usage = DatabaseUsage::None;
    SignatureLevel signatureLevel = SignatureLevel::Default;
    std::string arch = "x86_64";
    std::vector<std::string> dependencies;
    std::string localPkgDir;
    std::string localDbDir;
    AtomicDateTime lastUpdate;
    bool syncFromMirror = false;
    bool toBeDiscarded = false;

private:
    std::unique_ptr<DatabaseStorage> m_storage;
};

inline bool Database::isStaging() const
{
    return isStaging(nameWithoutDebugSuffix());
}

inline bool Database::isTesting() const
{
    return isTesting(nameWithoutDebugSuffix());
}

inline bool Database::isDebug() const
{
    return isDebug(name);
}

inline PackageSearchResult::PackageSearchResult()
    : db(nullptr)
    , id(0)
{
}

inline PackageSearchResult::PackageSearchResult(Database &database, const std::shared_ptr<Package> &package, StorageID id)
    : db(&database)
    , pkg(package)
    , id(id)
{
}

inline PackageSearchResult::PackageSearchResult(Database &database, std::shared_ptr<Package> &&package, StorageID id)
    : db(&database)
    , pkg(std::move(package))
    , id(id)
{
}

inline PackageSearchResult::PackageSearchResult(Database &database, Package &&package, StorageID id)
    : db(&database)
    , pkg(std::make_shared<Package>(std::move(package)))
    , id(id)
{
}

inline bool PackageSearchResult::operator==(const PackageSearchResult &other) const
{
    const auto *const *const db1 = std::get_if<Database *>(&db);
    const auto *const *const db2 = std::get_if<Database *>(&other.db);
    if (!db1 || !db2) {
        return false;
    }
    return ((!*db1 && !*db2) || (*db1 && *db2 && (**db1).name == (**db2).name)) && pkg == other.pkg;
}

inline PackageBaseSearchResult::PackageBaseSearchResult(const Database &database, const PackageBase &package, StorageID id)
    : db(&database)
    , pkg(&package)
    , id(id)
{
}

} // namespace LibPkg

namespace std {

template <> struct hash<LibPkg::PackageSearchResult> {
    std::size_t operator()(const LibPkg::PackageSearchResult &res) const
    {
        using std::hash;
        const std::string *dbName = nullptr;
        if (const auto *const dbInfo = std::get_if<LibPkg::DatabaseInfo>(&res.db)) {
            dbName = &dbInfo->name;
        } else if (const auto *const db = std::get<LibPkg::Database *>(res.db)) {
            dbName = &db->name;
        }
        return ((hash<string>()(dbName ? *dbName : string()) ^ (hash<std::shared_ptr<LibPkg::PackageBase>>()(res.pkg) << 1)) >> 1);
    }
};

} // namespace std

namespace ReflectiveRapidJSON {

namespace JsonReflector {

// declare custom (de)serialization for PackageSearchResult
template <>
LIBPKG_EXPORT void push<LibPkg::PackageSearchResult>(
    const LibPkg::PackageSearchResult &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator);
template <>
LIBPKG_EXPORT void pull<LibPkg::PackageSearchResult>(LibPkg::PackageSearchResult &reflectable,
    const RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>> &value, JsonDeserializationErrors *errors);

// declare custom (de)serialization for PackageBaseSearchResult
template <>
LIBPKG_EXPORT void push<LibPkg::PackageBaseSearchResult>(
    const LibPkg::PackageBaseSearchResult &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator);
template <>
LIBPKG_EXPORT void pull<LibPkg::PackageBaseSearchResult>(LibPkg::PackageBaseSearchResult &reflectable,
    const RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>> &value, JsonDeserializationErrors *errors);

// declare custom (de)serialization for AtomicDateTime
template <>
LIBPKG_EXPORT void push<LibPkg::AtomicDateTime>(
    const LibPkg::AtomicDateTime &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator);
template <>
LIBPKG_EXPORT void pull<LibPkg::AtomicDateTime>(LibPkg::AtomicDateTime &reflectable,
    const RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>> &value, JsonDeserializationErrors *errors);

} // namespace JsonReflector

namespace BinaryReflector {

template <>
LIBPKG_EXPORT void writeCustomType<LibPkg::PackageSearchResult>(
    BinarySerializer &serializer, const LibPkg::PackageSearchResult &packageSearchResult, BinaryVersion version);
template <>
LIBPKG_EXPORT BinaryVersion readCustomType<LibPkg::PackageSearchResult>(
    BinaryDeserializer &deserializer, LibPkg::PackageSearchResult &packageSearchResult, BinaryVersion version);

template <>
LIBPKG_EXPORT void writeCustomType<LibPkg::AtomicDateTime>(
    BinarySerializer &serializer, const LibPkg::AtomicDateTime &packageSearchResult, BinaryVersion version);
template <>
LIBPKG_EXPORT BinaryVersion readCustomType<LibPkg::AtomicDateTime>(
    BinaryDeserializer &deserializer, LibPkg::AtomicDateTime &packageSearchResult, BinaryVersion version);

} // namespace BinaryReflector

} // namespace ReflectiveRapidJSON

#endif // LIBPKG_DATA_DATABASE_H
