#pragma once

#include <psemek/ecs/detail/query_cache.hpp>
#include <psemek/ecs/detail/ordered_component_hash.hpp>
#include <psemek/ecs/detail/table_container.hpp>
#include <psemek/util/hash_table.hpp>

#include <memory>

namespace psemek::ecs::detail
{

	struct query_cache_hash
	{
		std::uint64_t operator()(std::pair<util::span<util::uuid const>, util::span<util::uuid const>> const & uuids) const
		{
			return ordered_component_hash(uuids.first, uuids.second);
		}

		std::uint64_t operator()(std::shared_ptr<query_cache> const & cache) const
		{
			return cache->hash;
		}
	};

	struct query_cache_equal
	{
		bool operator()(std::pair<util::span<util::uuid const>, util::span<util::uuid const>> const & uuids, std::shared_ptr<query_cache> const & cache) const
		{
			if (!std::equal(uuids.first.begin(), uuids.first.end(), cache->with_uuids.begin(), cache->with_uuids.end()))
				return false;

			if (!std::equal(uuids.second.begin(), uuids.second.end(), cache->without_uuids.begin(), cache->without_uuids.end()))
				return false;

			return true;
		}

		bool operator()(std::shared_ptr<query_cache> const & cache1, std::shared_ptr<query_cache> const & cache2) const
		{
			return cache1.get() == cache2.get();
		}
	};

	// TODO: store query caches in a bitmask trie balanced by subtree size
	struct query_cache_container
	{
		std::shared_ptr<query_cache> create(util::span<util::uuid const> with_uuids, util::span<util::uuid const> without_uuids, table_container & table_container)
		{
			auto it = caches_.find(std::pair{with_uuids, without_uuids});
			if (it == caches_.end())
			{
				auto cache = std::make_shared<query_cache>();

				cache->with_uuids.assign(with_uuids.begin(), with_uuids.end());
				cache->without_uuids.assign(without_uuids.begin(), without_uuids.end());

				cache->hash = ordered_component_hash(with_uuids, without_uuids);

				table_container.apply([&](table & table){
					cache->add(&table);
				}, with_uuids, without_uuids);

				it = caches_.insert(std::move(cache)).first;
			}
			return *it;
		}

		template <typename Function, typename ContainsUUID>
		void apply(Function && function, ContainsUUID && contains_uuid)
		{
			for (auto & cache : caches_)
			{
				bool good = true;

				for (auto const & uuid : cache->with_uuids)
					if (!contains_uuid(uuid))
					{
						good = false;
						break;
					}

				for (auto const & uuid : cache->without_uuids)
					if (contains_uuid(uuid))
					{
						good = false;
						break;
					}

				if (!good)
					continue;

				function(*cache);
			}
		}

		std::size_t cache_count() const
		{
			return caches_.size();
		}

	private:
		util::hash_set<std::shared_ptr<query_cache>, query_cache_hash, query_cache_equal> caches_;
	};

}
