/**
 * @file HashMap.h
 * @author apio (cloudapio.eu)
 * @brief Map between keys and values with best-case constant time lookup.
 *
 * @copyright Copyright (c) 2023, the Luna authors.
 *
 */

#pragma once
#include <luna/HashTable.h>

/**
 * @brief Internal representation of a key-value pair in a HashMap.
 *
 * @tparam K The key type.
 * @tparam V The value type.
 */
template <typename K, typename V> struct HashPair
{
    K key;
    Option<V> value;

    /**
     * @brief Compare two HashPair objects's keys.
     *
     * @param other The HashPair to compare against.
     * @return true The keys match.
     * @return false The keys do not match.
     */
    bool operator==(const HashPair<K, V>& other) const
    {
        return key == other.key;
    }
};

/**
 * @brief Template specialization of hash() for HashPair objects.
 *
 * This function hashes only the key, ignoring the value.
 *
 * @tparam K The key type.
 * @tparam V The value type.
 * @param value The HashPair to hash.
 * @param salt A randomly generated salt to vary the output and avoid hash table attacks.
 * @return u64 The calculated hash.
 */
template <typename K, typename V> u64 hash(const HashPair<K, V>& value, u64 salt)
{
    return hash(value.key, salt);
}

/**
 * @brief A map between keys and values with best-case constant time lookup.
 *
 * @tparam K The key type.
 * @tparam V The value type.
 */
template <typename K, typename V> struct HashMap
{
  public:
    /**
     * @brief Try to insert a key-value pair into the HashMap.
     *
     * @param key The key to use.
     * @param value The value to use.
     * @return Result<bool> An error, true if the insertion succeeded, or false if the key already existed.
     */
    Result<bool> try_set(const K& key, V&& value)
    {
        return m_table.try_set(HashPair<K, V> { key, move(value) });
    }

    /**
     * @brief Try to insert a key-value pair into the HashMap.
     *
     * @param key The key to use.
     * @param value The value to use.
     * @return Result<bool> An error, true if the insertion succeeded, or false if the key already existed.
     */
    Result<bool> try_set(const K& key, const V& value)
    {
        return m_table.try_set(HashPair<K, V> { key, value });
    }

    /**
     * @brief Get the associated value for a key.
     *
     * @param key The key to use.
     * @return Option<V> The associated value, or an empty option if the key did not exist.
     */
    Option<V> try_get(const K& key)
    {
        auto* p = m_table.try_find(HashPair<K, V> { key, {} });
        if (!p) return {};
        return p->value;
    }

    /**
     * @brief Get a pointer to the associated value for a key inside the HashMap.
     *
     * This pointer should always be checked before usage.
     *
     * @param key The key to use.
     * @return V* A pointer to the associated value, or nullptr if the key did not exist.
     */
    V* try_get_ref(const K& key)
    {
        auto* p = m_table.try_find(HashPair<K, V> { key, {} });
        if (!p) return nullptr;
        return p->value.value_ptr();
    }

    /**
     * @brief Remove a key-value pair from this HashMap.
     *
     * @param key The key to use.
     * @return true The pair was successfully removed.
     * @return false The key did not exist.
     */
    bool try_remove(const K& key)
    {
        return m_table.try_remove(HashPair<K, V> { key, {} });
    }

    /**
     * @brief Return the number of key-value pairs that can currently fit in the HashMap.
     *
     * The number of actual entries will always be smaller than this (unless it is 0), since the HashMap grows before
     * the number of entries reaches the capacity.
     *
     * @return usize The current capacity.
     */
    usize capacity() const
    {
        return m_table.capacity();
    }

    /**
     * @brief Return the number of key-value pairs currently contained in this HashMap.
     *
     * @return usize The number of pairs contained.
     */
    usize size() const
    {
        return m_table.size();
    }

    /**
     * @brief Clear the HashMap.
     */
    void clear()
    {
        m_table.clear();
    }

  private:
    HashTable<HashPair<K, V>> m_table;
};