/*
 * Copyright (C) 2025 Dominik Drexler
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *<
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */

#ifndef TYR_COMMON_HASH_HPP_
#define TYR_COMMON_HASH_HPP_

#include "tyr/common/declarations.hpp"
#include "tyr/common/dynamic_bitset.hpp"
#include "tyr/common/observer_ptr.hpp"

#include <cmath>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <span>
#include <utility>
#include <variant>
#include <vector>

namespace tyr
{

/**
 * Forward declarations
 */

template<typename T>
inline void hash_combine(size_t& seed, const T& value) noexcept;

template<typename T, typename... Rest>
inline void hash_combine(size_t& seed, const Rest&... rest) noexcept;

template<typename... Ts>
inline size_t hash_combine(const Ts&... rest) noexcept;

/// @brief `Hash` is our custom hasher, like std::hash.
///
/// Forwards to std::hash by default.
/// Specializations can be injected into the namespace.
template<typename T>
struct Hash
{
    size_t operator()(const T& el) const noexcept { return std::hash<T> {}(el); }
};

template<>
struct Hash<::cista::offset::string>
{
    using Type = ::cista::offset::string;

    size_t operator()(const Type& el) const noexcept
    {
        size_t aggregated_hash = el.size();
        for (const auto& item : el)
            hash_combine(aggregated_hash, item);
        return aggregated_hash;
    }
};

template<typename T, template<typename> typename Ptr, bool IndexPointers, typename TemplateSizeType, class Allocator>
struct Hash<::cista::basic_vector<T, Ptr, IndexPointers, TemplateSizeType, Allocator>>
{
    using Type = ::cista::basic_vector<T, Ptr, IndexPointers, TemplateSizeType, Allocator>;

    size_t operator()(const Type& el) const noexcept
    {
        size_t aggregated_hash = el.size();
        for (const auto& item : el)
            hash_combine(aggregated_hash, item);
        return aggregated_hash;
    }
};

template<typename... Ts>
struct Hash<::cista::offset::variant<Ts...>>
{
    using Type = ::cista::offset::variant<Ts...>;

    size_t operator()(const Type& el) const noexcept
    {
        return el.apply([](auto&& arg) -> size_t { return Hash<std::remove_cvref_t<decltype(arg)>> {}(arg); });
    }
};

template<typename T>
struct Hash<::cista::optional<T>>
{
    using Type = ::cista::optional<T>;

    size_t operator()(const Type& el) const noexcept
    {
        if (!el.has_value())
            return 0x9e3779b97f4a7c15ULL;

        return Hash<T> {}(*el);
    }
};

/// @brief Hash specialization for double.
///
/// Returns fixed salt for NaN.
template<IsFloatingPoint T>
struct Hash<T>
{
    size_t operator()(const T& el) const noexcept
    {
        if (std::isnan(el))
            return 0x9e3779b97f4a7c15ULL;  // any fixed salt

        return std::hash<T> {}(el);
    }
};

/// @brief Hash specialization for std::array.
///
/// Combines the hashes of all elements in the array.
/// @tparam T
template<typename T, size_t N>
struct Hash<std::array<T, N>>
{
    size_t operator()(const std::array<T, N>& arr) const noexcept
    {
        size_t aggregated_hash = N;
        for (const auto& element : arr)
            hash_combine(aggregated_hash, element);
        return aggregated_hash;
    }
};

/// @brief Hash specialization for std::reference_wrapper.
///
/// Hashes whatever .get() returns.
/// @tparam T
template<typename T>
struct Hash<std::reference_wrapper<T>>
{
    size_t operator()(const std::reference_wrapper<T>& ref) const noexcept { return Hash<std::remove_cvref_t<T>> {}(ref.get()); }
};

/// @brief Hash specialization for std::set.
///
/// Combines the hashes of all elements in the set.
/// @tparam T
template<typename Key, typename Compare, typename Allocator>
struct Hash<std::set<Key, Compare, Allocator>>
{
    size_t operator()(const std::set<Key, Compare, Allocator>& set) const noexcept
    {
        std::size_t aggregated_hash = set.size();
        for (const auto& item : set)
            hash_combine(aggregated_hash, item);
        return aggregated_hash;
    }
};

/// @brief Hash specialization for std::map.
///
/// Combines the hashes of all elements in the map.
/// @tparam K is the key type.
/// @tparam V is the value type
template<typename Key, typename T, typename Compare, typename Allocator>
struct Hash<std::map<Key, T, Compare, Allocator>>
{
    size_t operator()(const std::map<Key, T, Compare, Allocator>& map) const noexcept
    {
        std::size_t aggregated_hash = map.size();
        for (const auto& item : map)
            hash_combine(aggregated_hash, item);
        return aggregated_hash;
    }
};

/// @brief Hash specialization for std::vector.
///
/// Combines the hashes of all elements in the vector.
/// @tparam T
template<typename T, typename Allocator>
struct Hash<std::vector<T, Allocator>>
{
    size_t operator()(const std::vector<T, Allocator>& vec) const noexcept
    {
        size_t aggregated_hash = vec.size();
        for (const auto& element : vec)
            hash_combine(aggregated_hash, element);
        return aggregated_hash;
    }
};

/// @brief Hash specialization for a std::pair.
///
/// Combines the hashes for first and second.
/// @tparam T1
/// @tparam T2
template<typename T1, typename T2>
struct Hash<std::pair<T1, T2>>
{
    size_t operator()(const std::pair<T1, T2>& pair) const noexcept { return hash_combine(pair.first, pair.second); }
};

/// @brief Hash specialization for a std::tuple.
///
/// Combines the hashes of all elements in the tuple.
/// @tparam ...Ts
template<typename... Ts>
struct Hash<std::tuple<Ts...>>
{
    size_t operator()(const std::tuple<Ts...>& tuple) const noexcept
    {
        size_t aggregated_hash = sizeof...(Ts);
        std::apply([&aggregated_hash](const Ts&... args) { (hash_combine(aggregated_hash, args), ...); }, tuple);
        return aggregated_hash;
    }
};

/// @brief Hash specialization for a std::variant.
///
/// Hashes the underlying object.
/// @tparam ...Ts
template<typename... Ts>
struct Hash<std::variant<Ts...>>
{
    size_t operator()(const std::variant<Ts...>& variant) const noexcept
    {
        return std::visit([](const auto& arg) { return Hash<std::remove_cvref_t<decltype(arg)>> {}(arg); }, variant);
    }
};

/// @brief Hash specialization for a std::optional.
///
/// Hashes the underlying object if it exists, otherwise, returns 0.
/// @tparam T
template<typename T>
struct Hash<std::optional<T>>
{
    size_t operator()(const std::optional<T>& optional) const noexcept { return optional.has_value() ? Hash<std::remove_cvref_t<T>> {}(optional.value()) : 0; }
};

/// @brief Hash specialization for a std::span.
///
/// Combines the hashes of the the pointer and size.
template<typename T, std::size_t Extent>
struct Hash<std::span<T, Extent>>
{
    size_t operator()(const std::span<T, Extent>& span) const noexcept
    {
        size_t aggregated_hash = span.size();
        for (const auto& x : span)
            hash_combine(aggregated_hash, x);
        return aggregated_hash;
    }
};

/// @brief std::hash specialization for types T that satisfy `Identifiable`.
/// Dereferences the underlying pointer before forwarding the call to the std::hash
/// specialization of `IdentifiableMembersView` of T to compute a hash based on all members.
/// @tparam T is the type.
template<typename T>
struct Hash<ObserverPtr<T>>
{
    size_t operator()(ObserverPtr<T> ptr) const noexcept { return Hash<std::remove_cvref_t<T>> {}(*ptr); }
};

template<std::unsigned_integral Block>
struct Hash<BitsetSpan<Block>>
{
    size_t operator()(const BitsetSpan<Block>& bitset_span) const noexcept
    {
        size_t aggregated_hash = bitset_span.num_bits();
        for (const auto& block : bitset_span.blocks())
            hash_combine(aggregated_hash, block);
        return aggregated_hash;
    }
};

/// @brief std::hash specialization for an `IdentifiableMembersView`
/// that computes a hash based on all members.
/// @tparam ...Ts are the types of all members.
template<Identifiable T>
struct Hash<T>
{
    using is_transparent = void;  // <-- enables hetero lookup

    size_t operator()(const T& element) const noexcept { return hash_combine(element.identifying_members()); }

    template<typename... Args>
    size_t operator()(const std::tuple<Args...>& view) const noexcept
    {
        return hash_combine(view);
    }
};

/**
 * Definitions
 */

/// @brief Computes that hash for and object and combines it into the seed using Boost's hash combine function.
/// @tparam T is the type of the object.
/// @param seed is the seed.
/// @param value is the object.
template<typename T>
inline void hash_combine(size_t& seed, const T& value) noexcept
{
    seed ^= Hash<std::remove_cvref_t<T>> {}(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
}

template<typename T, typename... Rest>
inline void hash_combine(size_t& seed, const Rest&... rest) noexcept
{
    (hash_combine(seed, rest), ...);
}

template<typename... Ts>
inline size_t hash_combine(const Ts&... rest) noexcept
{
    size_t seed = 0;
    (hash_combine(seed, rest), ...);
    return seed;
}

}

#endif
