/*******************************************************************************
***  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 "Account.h"
using pecunia::currency::Money;
using std::array;
using ::quint32;
using drn::accounting::Account;
using drn::accounting::AccountCode;
using drn::accounting::AccountTypes;
using drn::accounting::internal::roundBalance;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
using pecunia::currency::minorUnitDigits;
#include <pecunia/Rounders.h>
using pecunia::rounders::currency::even;
#include <QObject>
using ::QObject;
#include <QString>
using ::QString;

#include <ostream>
using std::ostream;
#include <utility>
using std::move;

#include "AccountingErrors.h"
using drn::accounting::AccountError;
#include <foundation/EnumMacros.hpp>
// using BEGIN_ENUMERATION_TO_STRING
// using MAP_ENUMERATION_TO_STRING
// using END_ENUMERATION_TO_STRING
#include <foundation/Optional.hpp>
using drn::foundation::Optional;
#include <internal/AccountTransactionKey.hpp>
using drn::accounting::internal::AccountTransactionKey;


void drn::accounting::internal::roundBalance(Money& balance)
{
	balance.round(&even, minorUnitDigits(balance.code()));
}

Account::Account() :
	type_{AccountTypes::Unknown},
	code_{},
	balance_{},
	isClosed_{false}
{}

Account::Account(const AccountTypes type, AccountCode code, const Iso4217Codes& currency) :
	type_{type},
	code_{move(code)},
	balance_{currency},
	isClosed_{false}
{
	if (this->type_ == AccountTypes::Unknown)
		throw AccountError{this->code_, QObject::tr("The account type cannot be unknown.")};

	if (this->balance_.code() == Iso4217Codes::XXX)
		throw AccountError{this->code_, QObject::tr("The currency code cannot be unknown.")};
}

Account::Account(
	const AccountTypes type,
	AccountCode code,
	const Money& balance,
	const bool isClosed
) :
	type_{type},
	code_{move(code)},
	balance_{balance},
	isClosed_{isClosed}
{
	if (this->type_ == AccountTypes::Unknown)
		throw AccountError{this->code_, QObject::tr("The account type cannot be unknown.")};

	if (AccountNumber::invalid_ == this->code_.number())
		throw AccountError{this->code_, QObject::tr("The account code number must be set.")};

	if (this->balance_.code() == Iso4217Codes::XXX)
		throw AccountError{this->code_, QObject::tr("The currency code cannot be unknown.")};
}

bool Account::operator==(const Account& other) const
{
	return this->type_ == other.type_
		&& this->code_ == other.code_
		&& this->balance_ == other.balance_
		&& this->isClosed_ == other.isClosed_;
}

bool Account::operator!=(const Account& other) const
{
	return ! (*this == other);
}

bool Account::operator<(const Account& other) const
{
	return this->code_ < other.code_;
}

const AccountTypes& Account::type() const
{
	return this->type_;
}

const AccountCode& Account::code() const
{
	return this->code_;
}

void Account::setCode(AccountCode code)
{
	if (this->isClosed_)
		throw AccountError{
			this->code_,
			QObject::tr("Cannot change the account code of an account that it is closed.")
		};

	if (AccountNumber::invalid_ == code.number())
		throw AccountError{
			this->code_,
			QObject::tr("Cannot change the account code of an account to be empty.")
		};
	this->code_ = move(code);
}

const Money& Account::balance() const
{
	return this->balance_;
}

const bool& Account::isClosed() const
{
	return this->isClosed_;
}

void Account::convertTo(const pecunia::currency::Iso4217Codes& currency)
{
	if (this->isClosed_)
		throw AccountError{
			this->code_,
			QObject::tr("Cannot convert the balance currency of an account that it is closed.")
		};

	if (currency == Iso4217Codes::XXX)
		throw AccountError{
			this->code_,
			QObject::tr("Cannot convert the balance to an unknown currency.")
		};
	Money newBalance{0, 0u, currency};
	newBalance += this->balance_;
	this->balance_.setCode(currency);
	this->balance_ = newBalance;
}

void Account::increase(const Money& amount, const AccountTransactionKey&)
{
	if (this->isClosed_)
		throw AccountError{
			this->code_,
			QObject::tr("Cannot increase the balance while the account is closed.")
		};

	if (amount < 0)
		throw AccountError{
			this->code_,
			QObject::tr("Cannot increase the balance by a negative amount.")
		};
	this->balance_ += amount;

	if (this->balance_.code() != amount.code())
		roundBalance(this->balance_);
}

void Account::decrease(const Money& amount, const AccountTransactionKey&)
{
	if (this->isClosed_)
		throw AccountError{
			this->code_,
			QObject::tr("Cannot decrease the balance while the account is closed.")
		};

	if (amount < 0)
		throw AccountError{
			this->code_,
			QObject::tr("Cannot decrease the balance by a negative amount.")
		};
	this->balance_ -= amount;

	if (this->balance_.code() != amount.code())
		roundBalance(this->balance_);
}

void Account::close()
{
	this->isClosed_ = true;
}

void Account::open()
{
	this->isClosed_ = false;
}

void Account::setParent(Optional<AccountNumber> parent)
{
	if (this->isClosed_)
		throw AccountError{
			this->code_,
			QObject::tr("Cannot set the parent while the account is closed.")
		};
	this->code_.setParent(move(parent));
}

ostream& drn::accounting::operator<<(ostream& o, const Account& a)
{
	return o << "Type (" << a.type() << "), Code (" << a.code() << "), Balance ("
			<< to_string(a.balance()) << ")";
}
