/*******************************************************************************
***  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/>.    ***
*******************************************************************************/
#include "BankMap.h"
using std::map;
using std::set;
using drn::banking::Bank;
using drn::banking::BankMap;
using drn::banking::BankName;

#include <QObject>
using ::QObject;
#include <QString>
using ::QString;

#include <algorithm>
using std::find_if;
#include <cassert>
// using assert
#include <exception>
using std::exception;
#include <ostream>
using std::ostream;
#include <utility>
using std::move;

#include <accounting/Account.h>
using drn::accounting::AccountNumber;
using drn::accounting::presentationText;
#include "BankingErrors.h"
using drn::banking::BankError;
#include <foundation/Optional.hpp>
using drn::foundation::Optional;


BankMap::BankMap(map<BankName, Bank> banks) :
	map<BankName, Bank>{move(banks)}
{
	for (const auto& nameBank : *this)
	{
		if (nameBank.first != nameBank.second.name_)
			throw BankError{
				nameBank.first,
				QObject::tr(
					"The key name does not match the value name, %1."
				).arg(nameBank.second.name_.institution())
			};

		for (const auto& accountNumber : nameBank.second.accounts())
		{
			if (AccountNumber::invalid_ == accountNumber)
				throw BankError{
					nameBank.first,
					QObject::tr("A default account cannot be associated with a bank.")
				};

			for (const auto& otherNameBank : *this)
				if (
					nameBank.first != otherNameBank.first
					&& otherNameBank.second.accounts().count(accountNumber) != 0
				)
					throw BankError{
						nameBank.first,
						QObject::tr(
							"The account '%1' is associated with the bank '%2' and cannot be "
								"used again."
						)
							.arg(presentationText(accountNumber))
							.arg(otherNameBank.first.institution())
					};
		}
	}
}

const Bank& BankMap::bank(const BankName& bn) const
try
{
	return this->at(bn);
}
catch (const exception& error)
{
	throw BankError{bn, QObject::tr("Failed to locate a bank by that name."), error};
}

void BankMap::add(Bank b)
{
	const auto bankName{b.name_};

	if (b.isClosed())
		throw BankError{bankName, QObject::tr("Cannot add a bank that is already closed.")};

	if (this->hasBank(bankName))
		throw BankError{bankName, QObject::tr("Cannot add a bank that already exists.")};

	for (const auto& nameBank : *this)
		for (const auto& accountCode : nameBank.second.accounts())
			if (b.accounts().count(accountCode) != 0)
				throw BankError{
					bankName,
					QObject::tr(
						"The account '%1' is already associated with the bank '%2' and cannot be "
								"added again."
					)
						.arg(presentationText(accountCode))
						.arg(nameBank.first.institution())
				};
	(*this)[bankName] = move(b);
}

void BankMap::add(const BankName& name, const AccountNumber& number)
{
	if ( ! this->hasBank(name))
		throw BankError{
			name,
			QObject::tr(
				"Cannot add the account number, %1, as the bank that does not exist."
			).arg(presentationText(number))
		};

	const auto checkAssociation{this->find(number)};
	if (checkAssociation.hasValue())
		throw BankError{
			name,
			QObject::tr(
				"Cannot add the account number, %1, as the bank '%2' is already associated with "
					"that number."
			)
				.arg(presentationText(number))
				.arg(checkAssociation->name_.institution())
		};
	this->at(name).add(number);
}

bool BankMap::hasBank(const BankName& name) const
{
	return this->count(name) != 0;
}

Optional<Bank> BankMap::find(const AccountNumber& number) const
{
	const auto found{
		find_if(
			this->cbegin(),
			this->cend(),
			[&number] (const auto& nameBank)
			{
				return nameBank.second.accounts().find(number)
					!= nameBank.second.accounts().cend();
			}
		)
	};

	if (found == this->cend())
		return {};
	return {found->second};
}

void BankMap::close(const BankName& name)
{
	if ( ! this->hasBank(name))
		throw BankError{name, QObject::tr("Cannot close a bank that does not exist.")};
	this->at(name).close();
}

void BankMap::open(const BankName& name)
{
	if ( ! this->hasBank(name))
		throw BankError{name, QObject::tr("Cannot open a bank that does not exist.")};
	this->at(name).open();
}

void BankMap::remove(const BankName& name)
{
	if (this->erase(name) != 1)
		throw BankError{name, QObject::tr("Cannot remove a bank that does not exist.")};
}

void BankMap::remove(const BankName& name, const AccountNumber& number)
{
	if ( ! this->hasBank(name))
		throw BankError{
			name,
			QObject::tr(
				"Cannot remove the account number, %1, as the bank that does not exist."
			).arg(presentationText(number))
		};
	this->at(name).remove(number);
}

void BankMap::rename(const BankName& original, const BankName& changed)
{
	if ( ! this->hasBank(original))
		throw BankError{
			original,
			QObject::tr("Cannot rename to '%1' when the bank does not exist.").arg(changed.institution())
		};

	if (this->hasBank(changed))
		throw BankError{
			original,
			QObject::tr("Cannot rename to '%1' as it already exists.").arg(changed.institution())
		};
	auto& bank{this->at(original)};

	if (bank.isClosed())
		throw BankError{original, QObject::tr("Cannot rename a bank when it is closed.")};
	bank.name_ = changed;
	(*this)[changed] = move(bank);
	#pragma GCC diagnostic push
	#pragma GCC diagnostic ignored "-Wunused-variable"
	// Justification: This is a sanity check.
	const auto erased{this->erase(original)};
	assert(erased == 1 && "Failed to remove the original bank.");
	#pragma GCC diagnostic pop
}

void BankMap::update(Bank b)
{
	const auto name{b.name_};

	if ( ! this->hasBank(name))
		throw BankError{name, QObject::tr("Cannot update a bank that does not exist.")};

	if (b.isClosed())
		throw BankError{name, QObject::tr("Cannot update to a closed bank.")};

	if (this->at(name).isClosed())
		throw BankError{name, QObject::tr("Cannot update a bank that is closed.")};

	for (const auto& number : b.accounts())
	{
		const auto checkAssociation{this->find(number)};

		if (checkAssociation.hasValue() && checkAssociation->name_ != b.name_)
			throw BankError{
				name,
				QObject::tr(
					"Cannot update the account number, %1, as the bank '%2' is already associated "
						"with that number."
				)
					.arg(presentationText(number))
					.arg(checkAssociation->name_.institution())
			};
	}
	(*this)[name] = move(b);
}

set<BankName> BankMap::names() const
{
	set<BankName> names{};

	for (const auto& bank : *this)
		names.insert(bank.first);
	return names;
}

bool drn::banking::operator==(const BankMap& lhs, const BankMap& rhs)
{
	return static_cast<const map<BankName, Bank>&>(lhs)
		== static_cast<const map<BankName, Bank>&>(rhs);
}

bool drn::banking::operator!=(const BankMap& lhs, const BankMap& rhs)
{
	return ! (lhs == rhs);
}

ostream& drn::banking::operator<<(ostream& o, const BankMap& bm)
{
	o << "Bank Map (";

	bool isFirst{true};
	for (const auto& nameBank : static_cast<const map<BankName, Bank>&>(bm))
		o << (isFirst ? isFirst = false, "" : ", ") << nameBank.first << "=>" << nameBank.second;
	return o << ')';
}
