/*******************************************************************************
***  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 "Transaction.h"
using pecunia::currency::Money;
using pecunia::currency::swap;
using ::QDate;
using ::QString;
using ::quint32;
using drn::accounting::Account;
using drn::accounting::AccountCode;
using drn::accounting::internal::roundBalance;
using drn::accounting::presentationText;
using drn::accounting::Transaction;
using drn::accounting::TransactionNumber;
using drn::accounting::TransactionStatuses;
using drn::foundation::Optional;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
using pecunia::currency::toStdString;
#include <pecunia/MoneyTypes.h>
using pecunia::converter;
#include <QObject>
using ::QObject;

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

#include "AccountingErrors.h"
using drn::accounting::TransactionError;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/EnumMacros.hpp>
// using BEGIN_ENUMERATION_TO_OTHER
// using BEGIN_STRING_TO_ENUMERATION
// using MAP_ENUMERATION_TO_OTHER
// using MAP_STRING_TO_ENUMERATION
// using END_ENUMERATION_TO_OTHER
// using END_STRING_TO_ENUMERATION
#include <foundation/PresentationText.h>
using drn::foundation::presentationText;
#include <internal/AccountTransactionKey.hpp>
using drn::accounting::internal::AccountTransactionKey;


//{ TransactionNumber

const quint32 TransactionNumber::invalid_{0u};

TransactionNumber::TransactionNumber() :
	value_{TransactionNumber::invalid_}
{}

TransactionNumber::TransactionNumber(const quint32& value) :
	value_{value}
{

	if (this->value_ == TransactionNumber::invalid_)
		throw Error{QObject::tr("The transaction number cannot be invalid.")};
}

bool TransactionNumber::operator==(const TransactionNumber& other) const
{
	return *this == other.value_;
}

bool TransactionNumber::operator==(const quint32 value) const
{
	return this->value_ == value;
}

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

bool TransactionNumber::operator!=(const quint32 value) const
{
	return ! (*this == value);
}

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

bool TransactionNumber::operator>(const TransactionNumber& other) const
{
	return this->value_ > other.value_;
}

TransactionNumber& TransactionNumber::operator++()
{
	++this->value_;
	return *this;
}

TransactionNumber TransactionNumber::operator++(int)
{
	const TransactionNumber preIncrement{this->value_++};
	return preIncrement;
}

const quint32& TransactionNumber::integer() const
{
	return this->value_;
}

//}

//{ Transaction

Transaction::Transaction(
	const QDate& occurredOn,
	const AccountCode& debiting,
	const AccountCode& crediting,
	const Money& amount,
	Optional<QString> description,
	Optional<TransactionNumber> id,
	const TransactionStatuses& status
) :
	occurredOn_{occurredOn},
	debiting_{debiting},
	crediting_{crediting},
	amount_{amount},
	description_{move(description)},
	number_{move(id)},
	status_{status}
{
	if ( ! this->occurredOn_.isValid())
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr("The occurrence date must be set.")
		};

	if (this->debiting_ == this->crediting_)
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr("The debiting and crediting account codes cannot be the same.")
		};

	if (this->amount_ < 0)
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr("The amount applied to a transaction cannot be less than zero.")
		};
}

Transaction& Transaction::operator=(const Transaction& other)
{
	if (this != &other)
	{
		this->occurredOn_ = other.occurredOn_;
		this->number_ = other.number_;
		this->debiting_ = other.debiting_;
		this->crediting_ = other.crediting_;
		auto copy{other.amount_};
		swap(this->amount_, copy);
		this->status_ = other.status_;
		this->description_ = other.description_;
	}
	return *this;
}

Transaction& Transaction::operator=(Transaction&& other) noexcept
{
	if (this != &other)
	{
		this->occurredOn_ = move(other.occurredOn_);
		this->number_ = move(other.number_);
		this->debiting_ = move(other.debiting_);
		this->crediting_ = move(other.crediting_);
		auto copy{move(other.amount_)};
		swap(this->amount_, copy);
		this->status_ = move(other.status_);
		this->description_ = move(other.description_);
	}
	return *this;
}

bool Transaction::operator==(const Transaction& other) const
{
	return this->occurredOn_ == other.occurredOn_
		&& this->number_ == other.number_
		&& this->debiting_ == other.debiting_
		&& this->crediting_ == other.crediting_
		&& this->amount_ == other.amount_
		&& this->status_ == other.status_
		&& this->description_ == other.description_;
}

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

bool Transaction::operator<(const Transaction& other) const
{
	const auto lhsNumber{this->number_.valueOrDefault()};
	const auto rhsNumber{other.number_.valueOrDefault()};
	return tie(this->occurredOn_, lhsNumber, this->crediting_, this->debiting_, this->amount_)
		<  tie(other.occurredOn_, rhsNumber, other.crediting_, other.debiting_, other.amount_);
}

const QDate& Transaction::occurredOn() const
{
	return this->occurredOn_;
}

const AccountCode& Transaction::debiting() const
{
	return this->debiting_;
}

const AccountCode& Transaction::crediting() const
{
	return this->crediting_;
}

const Money& Transaction::amount() const
{
	return this->amount_;
}

const Optional<QString>& Transaction::description() const
{
	return this->description_;
}

const Optional<TransactionNumber>& Transaction::number() const
{
	return this->number_;
}

void Transaction::setNumber(TransactionNumber value, const AccountTransactionKey&)
{
	if (this->number_.hasValue())
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr("The transaction number cannot be changed once set.")
		};
	this->number_ = value;
}

const TransactionStatuses& Transaction::status() const
{
	return this->status_;
}

void Transaction::markUnreconciled()
{
	switch (this->status_)
	{
	case TransactionStatuses::Unreconciled:
	case TransactionStatuses::Cleared:
		this->status_ = TransactionStatuses::Unreconciled;
		break;
	case TransactionStatuses::Reconciled:
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr(
				"A transaction cannot be marked unreconciled once it's been marked reconciled."
			)
		};
	};
}

void Transaction::markCleared()
{
	switch (this->status_)
	{
	case TransactionStatuses::Unreconciled:
	case TransactionStatuses::Cleared:
		this->status_ = TransactionStatuses::Cleared;
		break;
	case TransactionStatuses::Reconciled:
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr("A transaction cannot be marked cleared once it's been marked reconciled.")
		};
	};
}

void Transaction::markReconciled()
{
	this->status_ = TransactionStatuses::Reconciled;
}

void Transaction::changeDebit(const AccountCode& code, const AccountTransactionKey&)
{
	if (this->crediting_ == code)
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr(
				"Cannot change the debit account code as it is the same as the credit account code."
			)
		};
	this->debiting_ = code;
}

void Transaction::changeCredit(const AccountCode& code, const AccountTransactionKey&)
{
	if (this->debiting_ == code)
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr(
				"Cannot change the credit account code as it is the same as the debit account code."
			)
		};
	this->crediting_ = code;
}

void Transaction::convertAmount(const Iso4217Codes& changeto, const AccountTransactionKey&)
{
	if (this->amount_.code() == changeto)
		throw TransactionError{
			this->number_,
			*this,
			QObject::tr("Cannot convert the amount to the same currency.")
		};
	Money converted{changeto};
	converted = this->amount_;
	roundBalance(converted);
	QString description{this->description_.valueOrDefault()};
	description.append(
		QObject::tr(" Converted from %1 at the exchange rate of %2 %3.")
			.arg(QString::fromStdString(toStdString(this->amount_.code())))
			.arg(QString::fromStdString(toStdString(changeto)))
			.arg(QString::number(converter(this->amount_.code(), changeto)))
	);
	this->description_ = move(description);
	this->amount_.setCode(changeto);
	this->amount_ = move(converted);
}

//}

TransactionStatuses drn::accounting::toTransactionStatuses(const QString& weak)
{
	BEGIN_OTHER_TO_ENUMERATION(TransactionStatuses);
		MAP_OTHER_TO_ENUMERATION(weak, "Unreconciled", TransactionStatuses::Unreconciled);
		MAP_OTHER_TO_ENUMERATION(weak, "Cleared", TransactionStatuses::Cleared);
		MAP_OTHER_TO_ENUMERATION(weak, "Reconciled", TransactionStatuses::Reconciled);
	END_OTHER_TO_ENUMERATION(weak, TransactionStatuses);
}

ostream& drn::accounting::operator<<(ostream& o, const TransactionNumber& tn)
{
	return o << "Integer (" << presentationText(tn).toStdString() << ')';
}

ostream& drn::accounting::operator<<(ostream& o, const TransactionStatuses& ts)
{
	return o << "Value (" << presentationText(ts).toStdString() << ')';
}

ostream& drn::accounting::operator<<(ostream& o, const Transaction& t)
{
	return o << "Occurred On (" << t.occurredOn().toString().toStdString() << "), Number ("
		<< t.number() << "), Description (" << t.description() <<  "), Dr (" << t.debiting()
		<< "), Cr (" << t.crediting() << "), Amount (" << t.amount() << "), Recl (" << t.status()
		<< ')';
}

QString drn::accounting::presentationText(const TransactionNumber& tn)
{
	return QString::number(tn.integer());
}

QString drn::accounting::presentationText(const TransactionStatuses& ts)
{
	BEGIN_ENUMERATION_TO_OTHER(ts);
		MAP_ENUMERATION_TO_OTHER(TransactionStatuses::Unreconciled, "Unreconciled");
		MAP_ENUMERATION_TO_OTHER(TransactionStatuses::Cleared, "Cleared");
		MAP_ENUMERATION_TO_OTHER(TransactionStatuses::Reconciled, "Reconciled");
	END_ENUMERATION_TO_OTHER(ts, TransactionStatuses);
}

QString drn::accounting::presentationText(const Transaction& t)
{
	return QObject::tr(
		"%1 #%2 Desc '%3' %4: %5, %6: %7 for %8 Recl: %9"
	)
		.arg(t.occurredOn().toString())
		.arg(
			t.number().hasValue()
				? presentationText(*t.number())
				: QObject::tr("Not Posted")
		)
		.arg(t.description().hasValue() ? *t.description() : QStringLiteral(""))
		.arg(debitAbbreviation())
		.arg(presentationText(t.debiting()))
		.arg(creditAbbreviation())
		.arg(presentationText(t.crediting()))
		.arg(::presentationText(t.amount()))
		.arg(presentationText(t.status()));
}

const QString& drn::accounting::creditAbbreviation()
{
	const static QString abbreviation{"Cr"};
	return abbreviation;
}

const QString& drn::accounting::debitAbbreviation()
{
	const static QString abbreviation{"Dr"};
	return abbreviation;
}
