/*******************************************************************************
***  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_BUDGETING_BUDGETEDMONEYMAP_HPP_
#define DRN_BUDGETING_BUDGETEDMONEYMAP_HPP_

#include <pecunia/Math.h>
#include <pecunia/Money.h>
#include <QObject>

#include <algorithm>
#include <chrono>
#include <map>
#include <set>
#include <type_traits>
#include <utility>

#include "Algorithm.h"
#include "BudgetingErrors.h"
#include "BudgetSource.h"
#include "Constants.hpp"
#include "EventFrequency.h"


namespace drn
{
namespace budgeting
{

class BudgetedMoney;

/**
 * @brief Maps all the budgeted monies to their source.
 *
 * @tparam BudgetedMoneyType This is the type that is mapped to a budget item source. This type
 * must derive from the BudgetedMoney class.
 */
template<typename BudgetedMoneyType>
struct BudgetedMoneyMap : private std::map<BudgetSource, BudgetedMoneyType>
{
	static_assert(
		std::is_base_of<BudgetedMoney, BudgetedMoneyType>::value,
		"The BudgetedMoneyType must derive from BudgetedMoney."
	);

	using std::map<BudgetSource, BudgetedMoneyType>::at;
	using std::map<BudgetSource, BudgetedMoneyType>::cbegin;
	using std::map<BudgetSource, BudgetedMoneyType>::cend;
	using typename std::map<BudgetSource, BudgetedMoneyType>::const_iterator;
	using typename std::map<BudgetSource, BudgetedMoneyType>::const_pointer;
	using typename std::map<BudgetSource, BudgetedMoneyType>::const_reference;
	using typename std::map<BudgetSource, BudgetedMoneyType>::const_reverse_iterator;
	using typename std::map<BudgetSource, BudgetedMoneyType>::difference_type;
	using std::map<BudgetSource, BudgetedMoneyType>::empty;
	using typename std::map<BudgetSource, BudgetedMoneyType>::key_compare;
	using typename std::map<BudgetSource, BudgetedMoneyType>::key_type;
	using typename std::map<BudgetSource, BudgetedMoneyType>::mapped_type;
	using std::map<BudgetSource, BudgetedMoneyType>::size;
	using typename std::map<BudgetSource, BudgetedMoneyType>::size_type;
	using typename std::map<BudgetSource, BudgetedMoneyType>::value_type;

	BudgetedMoneyMap(std::map<BudgetSource, BudgetedMoneyType> budgetedMonies = {});
	const_iterator begin() const;
	const_iterator end() const;
	/**
	 * @brief Determines if a specific budgeted money exists within the budgeted monies.
	 *
	 * @param source The source of the budgeted money to check.
	 *
	 * @return True when the budgeted money exists, false else-wise.
	 */
	bool has(const BudgetSource& source) const;
	/**
	 * @brief Places the new budgeted money into the budgeted monies indexed by its source.
	 *
	 * @exception BudgetItemError When the supplied budgeted money already exists in the
	 * budgeted monies.
	 *
	 * @param added The new budgeted money that is placed into the budgeted monies.
	 */
	void add(BudgetedMoneyType added);
	/**
	 * Changes the source of an budgeted money, and updates it's mapping.
	 *
	 * @exception BudgetItemError When the supplied current source does not exist.
	 * @exception BudgetItemError When the supplied changed source already exists.
	 *
	 * @param original The current source of the budgeted money.
	 * @param changed The new source of the budgeted money.
	 */
	void rename(const BudgetSource& original, BudgetSource changed);
	/**
	 * @brief Changes the budgeted money to the newly supplied one indexed by the source of the
	 * budgeted money. When the new budgeted money is not in the same currency as the existing
	 * entry, the new budgeted money will replace the existing entry, i.e. no currency conversions
	 * will take place.
	 *
	 * @exception BudgetItemError When the supplied source source does not exist.
	 *
	 * @param updated The new budgeted money to update the indexed entry to.
	 */
	void update(BudgetedMoneyType updated);
	/**
	 * @brief Removes an budgeted money from the budgeted monies.
	 *
	 * @exception BudgetItemError When the supplied source does not exist.
	 *
	 * @param source The budgeted money source to remove.
	 */
	void remove(const BudgetSource& source);
	/**
	 * @brief Equality operator compares all members.
	 *
	 * @param other The other object whose members should all be equal.
	 *
	 * @return When all member variables are the same it'll give true, else false.
	 */
	bool operator==(const BudgetedMoneyMap& other) const;
	/**
	 * @brief Calculates the total amount of budgeted monies.
	 *
	 * @param code The issued currency to calculate the monies in.
	 * @param period The frequency period all monies should be tabulated in.
	 * @param hrInWeek The number of hours considered in a full week.
	 * @param dayInWeek The number of days considered in a full week.
	 *
	 * @return The sum of the amounts.
	 */
	pecunia::currency::Money total(
		const pecunia::currency::Iso4217Codes& code,
		const EventFrequency period,
		const std::chrono::hours hrInWeek = std::chrono::hours{maximumHoursInAWeek},
		const foundation::Days dayInWeek = foundation::Days{maximumDaysInAWeek}
	) const;
	/**
	 * @brief An accessor to all the known budgeted money sources.
	 *
	 * @return The known sources.
	 */
	std::set<BudgetSource> sources() const;
};

}}

template<typename BudgetedMoneyType>
drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::BudgetedMoneyMap(
	std::map<BudgetSource, BudgetedMoneyType> monies
) :
	std::map<BudgetSource, BudgetedMoneyType>{std::move(monies)}
{}

template<typename BudgetedMoneyType>
typename drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::const_iterator
		drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::begin() const
{
	return this->std::map<BudgetSource, BudgetedMoneyType>::begin();
}

template<typename BudgetedMoneyType>
typename drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::const_iterator
		drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::end() const
{
	return this->std::map<BudgetSource, BudgetedMoneyType>::end();
}

template<typename BudgetedMoneyType>
bool drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::has(const BudgetSource& source) const
{
	return this->count(source) > 0;
}

template<typename BudgetedMoneyType>
void drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::add(BudgetedMoneyType added)
{
	if (added.source().isEmpty())
		throw BudgetItemError{
			added.source(),
			::QObject::tr("The new budgeted money item source cannot be empty.")
		};

	if (this->has(added.source()))
		throw BudgetItemError{
			added.source(),
			::QObject::tr("The new budgeted money item source already exists.")
		};
	auto source{added.source()};
	this->emplace(std::make_pair(std::move(source), std::move(added)));
}

template<typename BudgetedMoneyType>
void drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::rename(
	const BudgetSource& original,
	BudgetSource changed
)
{
	if ( ! this->has(original))
		throw BudgetItemError{
			original,
			::QObject::tr("Cannot rename the budgeted money item source as it does not exist.")
		};
	auto money{(*this)[original]};

	if (this->has(changed))
		throw BudgetItemError{
			money.source(),
			::QObject::tr(
				"Cannot rename the budgeted money item source, as the new source, %1, already "
					"exists."
			)
				.arg(changed.name())
		};
	money.setSource(changed);
	this->emplace(std::make_pair(std::move(changed), std::move(money)));
	this->remove(original);
}

template<typename BudgetedMoneyType>
void drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::update(BudgetedMoneyType updated)
{
	if ( ! this->has(updated.source()))
		throw BudgetItemError{
			updated.source(),
			::QObject::tr("Cannot update the budgeted money item as it does not exist.")
		};
	const auto source{updated.source()};

	if (this->at(source).amount().code() != updated.amount().code())
	{
		this->remove(source);
		this->add(std::move(updated));
	}
	else
		(*this)[source] = std::move(updated);
}

template<typename BudgetedMoneyType>
void drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::remove(const BudgetSource& source)
{
	if (this->erase(source) == 0)
		throw BudgetItemError{
			source,
			::QObject::tr("Failed to remove the budgeted money item as it does not exist.")
		};
}

template<typename BudgetedMoneyType>
bool drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::operator==(
	const BudgetedMoneyMap& other
) const
{
	return static_cast<const std::map<BudgetSource, BudgetedMoneyType>&>(*this) == other;
}

template<typename BudgetedMoneyType>
pecunia::currency::Money drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::total(
	const pecunia::currency::Iso4217Codes& code,
	const EventFrequency period,
	const std::chrono::hours hrInWeek,
	const foundation::Days dayInWeek
) const
{
	std::vector<pecunia::currency::Money> adjustedAmounts;
	std::transform(
		this->cbegin(),
		this->cend(),
		std::back_inserter(adjustedAmounts),
		[&period, &hrInWeek, &dayInWeek] (const auto& sourceBudgetedMoneyPair)
		{
			const auto adjustedMoney{
				adjustTo(
					period,
					sourceBudgetedMoneyPair.second.amount(),
					sourceBudgetedMoneyPair.second.period(),
					hrInWeek,
					dayInWeek
				)
			};
			return adjustedMoney;
		}
	);
	const pecunia::currency::Money sum{pecunia::math::sum(code, adjustedAmounts)};
	return sum;
}

template<typename BudgetedMoneyType>
std::set<drn::budgeting::BudgetSource> drn::budgeting::BudgetedMoneyMap<BudgetedMoneyType>::sources(
) const
{
	std::set<drn::budgeting::BudgetSource> sources;

	for (const auto& sourceBudgetedMoneyPair : *this)
		sources.insert(sourceBudgetedMoneyPair.first);
	return sources;
}

#endif
