/*
 * This file is part of OpenTTD.
 * OpenTTD 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, version 2.
 * OpenTTD 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 OpenTTD. If not, see <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
 */

/** @file hashtable.hpp Hash table support. */

#ifndef HASHTABLE_HPP
#define HASHTABLE_HPP

template <class TItem>
struct HashTableSlot {
	typedef typename TItem::Key Key; ///< Make Titem::Key a property of HashTable.

	TItem *first_item = nullptr;

	/** Clears the slot by simple forgetting its items. */
	inline void Clear()
	{
		this->first_item = nullptr;
	}

	/**
	 * Linear search for item with given key through the given blob.
	 * @param key The key to find.
	 * @return The found item, or \c nullptr.
	 */
	inline const TItem *Find(const Key &key) const
	{
		for (const TItem *item = this->first_item; item != nullptr; item = item->GetHashNext()) {
			if (item->GetKey() == key) {
				/* we have found the item, return it */
				return item;
			}
		}
		return nullptr;
	}

	/**
	 * Linear search for item with given key through the given blob.
	 * @param key The key to find.
	 * @return The found item, or \c nullptr.
	 */
	inline TItem *Find(const Key &key)
	{
		for (TItem *item = this->first_item; item != nullptr; item = item->GetHashNext()) {
			if (item->GetKey() == key) {
				/* we have found the item, return it */
				return item;
			}
		}
		return nullptr;
	}

	/**
	 * Add new item to the slot.
	 * @param new_item The item to insert.
	 */
	inline void Attach(TItem &new_item)
	{
		assert(new_item.GetHashNext() == nullptr);
		new_item.SetHashNext(this->first_item);
		this->first_item = &new_item;
	}

	/**
	 * Remove item from a slot.
	 * @param item_to_remove The item to remove.
	 * @return \c true iff the item could be removed.
	 */
	inline bool Detach(TItem &item_to_remove)
	{
		if (this->first_item == &item_to_remove) {
			this->first_item = item_to_remove.GetHashNext();
			item_to_remove.SetHashNext(nullptr);
			return true;
		}
		TItem *item = this->first_item;
		for (;;) {
			if (item == nullptr) {
				return false;
			}
			TItem *next_item = item->GetHashNext();
			if (next_item == &item_to_remove) break;
			item = next_item;
		}
		item->SetHashNext(item_to_remove.GetHashNext());
		item_to_remove.SetHashNext(nullptr);
		return true;
	}

	/**
	 * Remove and return item from a slot.
	 * @param key The key to search for.
	 * @return The removed item, or \c nullptr when nothing was removed.
	 */
	inline TItem *Detach(const Key &key)
	{
		/* do we have any items? */
		if (this->first_item == nullptr) {
			return nullptr;
		}
		/* is it our first item? */
		if (this->first_item->GetKey() == key) {
			TItem &ret_item = *this->first_item;
			this->first_item = this->first_item->GetHashNext();
			ret_item.SetHashNext(nullptr);
			return &ret_item;
		}
		/* find it in the following items */
		TItem *previous_item = this->first_item;
		for (TItem *item = this->first_item->GetHashNext(); item != nullptr; previous_item = item, item = item->GetHashNext()) {
			if (item->GetKey() == key) {
				/* we have found the item, unlink and return it */
				previous_item->SetHashNext(item->GetHashNext());
				item->SetHashNext(nullptr);
				return item;
			}
		}
		return nullptr;
	}
};

/**
 * class HashTable<Titem, HASH_BITS> - simple hash table
 *  of pointers allocated elsewhere.
 *
 *  Supports: Add/Find/Remove of Titems.
 *
 *  Your Titem must meet some extra requirements to be HashTable
 *  compliant:
 *    - its constructor/destructor (if any) must be public
 *    - if the copying of item requires an extra resource management,
 *        you must define also copy constructor
 *    - must support nested type (struct, class or typedef) Titem::Key
 *        that defines the type of key class for that item
 *    - must support public method:
 *        const Key& GetKey() const; // return the item's key object
 *
 *  In addition, the Titem::Key class must support:
 *    - public method that calculates key's hash:
 *        int CalcHash() const;
 *    - public 'equality' operator to compare the key with another one
 *        bool operator==(const Key &other) const;
 */
template <class Titem, int Thash_bits_>
class HashTable {
public:
	typedef typename Titem::Key Tkey; ///< Make Titem::Key a property of HashTable.
	static constexpr int HASH_BITS = Thash_bits_; ///< Publish num of hash bits.
	static constexpr int CAPACITY = 1 << HASH_BITS; ///< And num of slots 2^bits.

protected:
	/**
	 * each slot contains pointer to the first item in the list,
	 *  Titem contains pointer to the next item - GetHashNext(), SetHashNext()
	 */
	using Slot = HashTableSlot<Titem>;

	Slot slots[CAPACITY]; ///< Data storage (array of blobs).
	int number_of_items = 0; ///< Item counter.

	/**
	 * Return hash for the given key modulo number of slots.
	 * @param key The key to consider.
	 * @return The hash of the key.
	 */
	static inline int CalcHash(const Tkey &key)
	{
		uint32_t hash = key.CalcHash();
		hash -= (hash >> 17);          // hash * 131071 / 131072
		hash -= (hash >> 5);           //   * 31 / 32
		hash &= (1 << HASH_BITS) - 1; //   modulo slots
		return hash;
	}

	/**
	 * Return hash for the given item modulo number of slots.
	 * @param item The item to consider.
	 * @return The hash of its key.
	 */
	static inline int CalcHash(const Titem &item)
	{
		return CalcHash(item.GetKey());
	}

public:
	/**
	 * Get the number of elements.
	 * @return The item count.
	 */
	inline int Count() const
	{
		return this->number_of_items;
	}

	/** Clear all items. */
	inline void Clear()
	{
		for (int i = 0; i < CAPACITY; i++) this->slots[i].Clear();
		this->number_of_items = 0;
	}

	/**
	 * Try to find an item by key.
	 * @param key The key to find.
	 * @return The found item, or \c nullptr.
	 */
	const Titem *Find(const Tkey &key) const
	{
		int hash = CalcHash(key);
		const Slot &slot = this->slots[hash];
		const Titem *item = slot.Find(key);
		return item;
	}

	/**
	 * Try to find an item by key.
	 * @param key The key to find.
	 * @return The found item, or \c nullptr.
	 */
	Titem *Find(const Tkey &key)
	{
		int hash = CalcHash(key);
		Slot &slot = this->slots[hash];
		Titem *item = slot.Find(key);
		return item;
	}

	/**
	 * Remove an item by key if found.
	 * @param key The key to search for.
	 * @return The popped element, or \c nullptr.
	 */
	Titem *TryPop(const Tkey &key)
	{
		int hash = CalcHash(key);
		Slot &slot = this->slots[hash];
		Titem *item = slot.Detach(key);
		if (item != nullptr) {
			this->number_of_items--;
		}
		return item;
	}

	/**
	 * Remove an item by key that must exist.
	 * @param key The key to search for.
	 * @return The popped element; never \c nullptr.
	 */
	Titem &Pop(const Tkey &key)
	{
		Titem *item = TryPop(key);
		assert(item != nullptr);
		return *item;
	}

	/**
	 * Remove an item if found.
	 * @param item The item to remove.
	 * @return \c true iff the item existed.
	 */
	bool TryPop(Titem &item)
	{
		const Tkey &key = item.GetKey();
		int hash = CalcHash(key);
		Slot &slot = this->slots[hash];
		bool ret = slot.Detach(item);
		if (ret) {
			this->number_of_items--;
		}
		return ret;
	}

	/**
	 * Remove an item that must exist.
	 * @param item The item to remove.
	 */
	void Pop(Titem &item)
	{
		[[maybe_unused]] bool ret = TryPop(item);
		assert(ret);
	}

	/**
	 * Add an item that may not exist.
	 * @param new_item The item to add.
	 */
	void Push(Titem &new_item)
	{
		int hash = CalcHash(new_item);
		Slot &slot = this->slots[hash];
		assert(slot.Find(new_item.GetKey()) == nullptr);
		slot.Attach(new_item);
		this->number_of_items++;
	}
};

#endif /* HASHTABLE_HPP */
