/*******************************************************************************
***  This file is part of Dux Rei Nummariae.                                 ***
***                                                                          ***
***  Copyright (C) 2017, 2018, 2019, 2020, 2021                              ***
***  John Schneiderman <Licensing _AT_ Schneiderman _DOT_ me>                ***
***                                                                          ***
***  This program is free software: you can redistribute it and/or modify it ***
***  under the terms of the GNU Affero 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 Affero General Public License for more details.             ***
***                                                                          ***
***  You should have received a copy of the GNU Affero General Public License***
***  along with this program. If not, see <http://www.gnu.org/licenses/>.    ***
*******************************************************************************/
#ifndef DRN_FOUNDATION_TYPEINDEXMAP_HPP_
#define DRN_FOUNDATION_TYPEINDEXMAP_HPP_

#include <QObject>
#include <QString>

#include <exception>
#include <ostream>
#include <typeindex>

#include "Error.h"


namespace drn
{
namespace foundation
{

/**
 * @brief A generic map container of typed-keys to their values.
 *
 * @tparam KeyType The type for the typed-key.
 * @tparam ValueType The type for the values to store under the typed-key
 */
template<typename KeyType, typename ValueType>
class TypeIndexMap : std::map<std::type_index, std::map<KeyType, ValueType>>
{
	std::size_t total_;

public:
	using std::map<std::type_index, std::map<KeyType, ValueType>>::begin;
	using std::map<std::type_index, std::map<KeyType, ValueType>>::cbegin;
	using std::map<std::type_index, std::map<KeyType, ValueType>>::cend;
	using std::map<std::type_index, std::map<KeyType, ValueType>>::clear;
	using std::map<std::type_index, std::map<KeyType, ValueType>>::empty;
	using std::map<std::type_index, std::map<KeyType, ValueType>>::end;
	using std::map<std::type_index, std::map<KeyType, ValueType>>::size;

	/**
	 * @brief Empty constructor for a valid object. Initalises all values to zero.
	 */
	TypeIndexMap();
	/**
	 * @brief Adds a value to the container. When the typed-key already exists, the value is
	 * updated. When the typed-key does not exist, a new entry is made.
	 *
	 * @tparam IndexType The index type to place the key under.
	 *
	 * @param key The key to place the value under.
	 * @param val The value to place into the container.
	 */
	template<typename IndexType>
	void add(const KeyType& key, ValueType val);
	/**
	 * @brief Determines if a value is located under a given typed-key.
	 *
	 * @tparam IndexType The index type the key is expected to be under.
	 *
	 * @param key The key to search for in the container.
	 *
	 * @return Gives true when the typed-key exists, false otherwise.
	 */
	template<typename IndexType>
	bool has(const KeyType& key) const;
	/**
	 * @brief Non-mutable accessor to the value under a typed-key.
	 *
	 * @exception Error When the value is not located under a typed-key.
	 *
	 * @tparam IndexType The index type the key is expected to be under.
	 *
	 * @param key The key to get the value from under.
	 *
	 * @return Gives the value located under the typed-key.
	 */
	template<typename IndexType>
	const ValueType& value(const KeyType& key) const;
	/**
	 * @brief Mutable accessor to the value under a typed-key.
	 *
	 * @exception Error When the value is not located under a typed-key.
	 *
	 * @tparam IndexType The index type the key is expected to be under.
	 *
	 * @param key The key to get the value from under.
	 *
	 * @return Gives the value located under the typed-key.
	 */
	template<typename IndexType>
	ValueType& value(const KeyType& key);
	/**
	 * @brief Removes a value from the container. When the value removed is the last one, the typed
	 * element is removed as well.
	 *
	 * @exception Error When the typed-key is not located.
	 *
	 * @tparam IndexType The index type the key is expected to be under.
	 *
	 * @param key The key to remove from the container.
	 */
	template<typename IndexType>
	void remove(const KeyType& key);
	/**
	 * @brief Gives the total number of elements in the container.
	 *
	 * @return The number of combined elements under each typed-key.
	 */
	std::size_t total() const;
};

template<typename KeyType, typename ValueType>
bool operator==(
	const TypeIndexMap<KeyType, ValueType>& lhs,
	const TypeIndexMap<KeyType, ValueType>& rhs
);
template<typename KeyType, typename ValueType>
bool operator!=(
	const TypeIndexMap<KeyType, ValueType>& lhs,
	const TypeIndexMap<KeyType, ValueType>& rhs
);
template<typename KeyType, typename ValueType>
std::ostream& operator<<(std::ostream& o, const TypeIndexMap<KeyType, ValueType>& tim);

}}

template<typename KeyType, typename ValueType>
drn::foundation::TypeIndexMap<KeyType, ValueType>::TypeIndexMap() :
	total_{0u}
{}

template<typename KeyType, typename ValueType>
template<typename IndexType>
void drn::foundation::TypeIndexMap<KeyType, ValueType>::add(
	const KeyType& key,
	ValueType val
)
{
	const std::type_index type{typeid(IndexType)};
	(*this)[type][key] = std::move(val);
	++this->total_;
}

template<typename KeyType, typename ValueType>
template<typename IndexType>
bool drn::foundation::TypeIndexMap<KeyType, ValueType>::has(const KeyType& key) const
{
	const std::type_index type{typeid(IndexType)};
	return this->count(type) > 0 && this->at(type).count(key) > 0;
}

template<typename KeyType, typename ValueType>
template<typename IndexType>
const ValueType& drn::foundation::TypeIndexMap<KeyType, ValueType>::value(const KeyType& key) const
try
{
	const std::type_index type{typeid(IndexType)};
	return this->at(type).at(key);
}
catch (const std::exception& error)
{
	throw drn::foundation::Error{
		::QObject::tr("Failed to find a non-mutable value under the type '%1' with the key '%2'.")
			.arg(typeid(IndexType).name())
			.arg(presentationText(key)),
		error
	};
}

template<typename KeyType, typename ValueType>
template<typename IndexType>
ValueType& drn::foundation::TypeIndexMap<KeyType, ValueType>::value(const KeyType& key)
try
{
	const std::type_index type{typeid(IndexType)};
	return this->at(type).at(key);
}
catch (const std::exception& error)
{
	throw drn::foundation::Error{
		::QObject::tr("Failed to find a mutable value under the type '%1' with the key '%2'.")
			.arg(typeid(IndexType).name())
			.arg(presentationText(key)),
		error
	};
}

template<typename KeyType, typename ValueType>
template<typename IndexType>
void drn::foundation::TypeIndexMap<KeyType, ValueType>::remove(const KeyType& key)
{
	const std::type_index type{typeid(IndexType)};
	const auto typeKeys{this->find(type)};

	if (typeKeys == this->cend())
		throw drn::foundation::Error{
			::QObject::tr(
				"Failed to remove the value under the key '%1' as the index type '%2' was not "
					"found."
			)
				.arg(presentationText(key))
				.arg(type.name())
		};
	const auto keyValue{typeKeys->second.find(key)};


	if (keyValue == typeKeys->second.cend())
		throw drn::foundation::Error{
			::QObject::tr(
				"Failed to remove the value under the key '%1' as the key was not found under "
					"the index type '%2'."
			)
				.arg(presentationText(key))
				.arg(type.name())
		};
	typeKeys->second.erase(keyValue);
	--this->total_;

	if (typeKeys->second.empty())
		this->erase(typeKeys);
}

template<typename KeyType, typename ValueType>
std::size_t drn::foundation::TypeIndexMap<KeyType, ValueType>::total() const
{
	return this->total_;
}

template<typename KeyType, typename ValueType>
bool drn::foundation::operator==(
	const drn::foundation::TypeIndexMap<KeyType, ValueType>& lhs,
	const drn::foundation::TypeIndexMap<KeyType, ValueType>& rhs
)
{
	if (lhs.total() != rhs.total())
		return false;

	for (
		auto lhsTypeKeyValues{lhs.cbegin()}, rhsTypeKeyValues{rhs.cbegin()} ;
		lhsTypeKeyValues != lhs.cend() && rhsTypeKeyValues != rhs.cend() ;
		++lhsTypeKeyValues, ++rhsTypeKeyValues
	)
		if ( ! (*lhsTypeKeyValues == *rhsTypeKeyValues))
			return false;
	return true;
}

template<typename KeyType, typename ValueType>
bool drn::foundation::operator!=(
	const drn::foundation::TypeIndexMap<KeyType, ValueType>& lhs,
	const drn::foundation::TypeIndexMap<KeyType, ValueType>& rhs
)
{
	return ! (lhs == rhs);
}

template<typename KeyType, typename ValueType>
std::ostream& drn::foundation::operator<<(
	std::ostream& o,
	const drn::foundation::TypeIndexMap<KeyType, ValueType>& tim
)
{
	o << "Type-Keys (";
	bool isFirstType{true};

	for (auto typeKeyValues{tim.cbegin()} ; typeKeyValues != tim.cend() ; ++typeKeyValues)
	{
		o << (isFirstType ? isFirstType = false, "" : ", ") << typeKeyValues->first.name() << '[';

		bool isFirstKey{true};
		for (const auto& keyValue : typeKeyValues->second)
			o << (isFirstKey ? isFirstKey = false, "" : ", ")  << keyValue.first << "=>"
					<< keyValue.second;
		o << ']';
	}
	return o << ')';
}

#endif
