/*******************************************************************************
***  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_ACCOUNTING_LEDGERBOOKS_H_
#define DRN_ACCOUNTING_LEDGERBOOKS_H_

#include <map>
#include <iosfwd>
#include <set>
#include <vector>

#include "Account.h"
#include "accounting_export.h"
#include <foundation/Optional.hpp>
#include "Transaction.h"


namespace pecunia
{
namespace currency
{

enum class Iso4217Codes : quint16;

}}

namespace drn
{
namespace foundation
{

template<typename>
class Optional;

}
namespace accounting
{

class AccountNumberGenerator;

/**
 * @brief This is responsible for recording the transactions relating to an entity's assets,
 * liabilities, capital, revenue, and expenses of a given account.
 */
struct DRN_ACCOUNTING_EXPORT LedgerAccount
{
	/**
	 * @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 LedgerAccount& other) const;

	/// The account the ledger is tracking.
	Account account_;
	/// Maps a transaction number to the transaction.
	std::map<TransactionNumber, Transaction> transactions_;
};
/**
 * @brief This is the manager that contains all the accounts for recording transactions relating to
 * an entity's assets, liabilities, capital, revenue, and expenses.
 */
class DRN_ACCOUNTING_EXPORT GeneralLedger
{
	friend DRN_ACCOUNTING_EXPORT std::ostream& operator<<(std::ostream&, const GeneralLedger&);

	void validateTransaction(const Transaction& txn) const;
	void performTransaction(const Transaction& transaction, const TransactionNumber& number);
	void validateAddAccount(const Account& account) const;
	void createOpeningAccount(
		AccountNumberGenerator& generator,
		const pecunia::currency::Iso4217Codes& code
	);

	std::map<AccountCode, LedgerAccount> ledgers_;
	/// Maps a transaction number to the account code for the debit account (first), and credit
	/// account (second).
	std::map<TransactionNumber, std::pair<AccountCode, AccountCode>> accountTransactions_;
	TransactionNumber nextNumber_;
	foundation::Optional<AccountCode> openingAccountCode_;

public:
	/**
	 * @brief Default construction of an empty valid general ledger.
	 */
	GeneralLedger();
	/**
	 * @brief Fully initalises the object to the supplied values.
	 *
	 * @exception AccountError When the opening account code is not present in the ledgers.
	 * @exception AccountError When the opening account code is not an AccountTypes::Capital type.
	 * @exception AccountError When the mapped account code does not match the ledger's account
	 * code.
	 * @exception AccountError When the account for a parent does not exist.
	 * @exception AccountError When a parent account refers to a child account.
	 * @exception TransactionError When a ledger account contains an invalid transaction number.
	 * @exception TransactionError When a ledger account contains a transaction for a debiting
	 * account that does not exist.
	 * @exception TransactionError When a ledger account contains a transaction for a crediting
	 * account that does not exist.
	 * @exception TransactionError When a ledger account contains a transaction that does not
	 * contain a matching transaction in the other ledger account.
	 * @exception TransactionError When a ledger account contains a transaction number that is
	 * not mapped to the same number.
	 *
	 * @param ledgers The ledgers to store in the general ledger.
	 * @param openingAccountCode The account code to use for the opening balance when opening an
	 * account.
	 */
	explicit GeneralLedger(
		std::map<AccountCode, LedgerAccount> ledgers,
		foundation::Optional<AccountCode> openingAccountCode = {}
	);
	/**
	 * @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 GeneralLedger& other) const;
	/**
	 * @brief Inequality operator compares all members.
	 *
	 * @param other The other object whose members should be not equal.
	 *
	 * @return When any member variable is not the same it'll give true, else false.
	 */
	bool operator!=(const GeneralLedger& other) const;
	/**
	 * @brief The current ledgers in the general ledger.
	 */
	const std::map<AccountCode, LedgerAccount>& ledgers() const;
	/**
	 * @brief Determines if an account ledger book exits in the general ledger.
	 *
	 * @param code The account code whose ledger status is desired.
	 *
	 * @return Gives true when the account ledger exists, false else-wise.
	 */
	bool hasLedger(const AccountCode& code) const;
	/**
	 * @brief Determines if an account ledger book exits in the general ledger.
	 *
	 * @param number The account number whose ledger status is desired.
	 *
	 * @return Gives true when the account ledger exists, false else-wise.
	 */
	bool hasLedger(const AccountNumber& number) const;
	/**
	 * @brief Provides access to the ledger of a specific account.
	 *
	 * @exception AccountError When the ledger book for the supplied account code does not exist.
	 *
	 * @param code The account code whose ledger is desired.
	 *
	 * @return The immutable account ledger book.
	 */
	const LedgerAccount& ledger(const AccountCode& code) const;
	/**
	 * @brief Provides access to the ledger of a specific account.
	 *
	 * @exception AccountError When the ledger book for the supplied account code does not exist.
	 *
	 * @param number The account number whose ledger is desired.
	 *
	 * @return The immutable account ledger book.
	 */
	const LedgerAccount& ledger(const AccountNumber& number) const;
	/**
	 * @brief Posts a supplied transaction to the debiting and crediting accounts.
	 *
	 * @exception TransactionError When the transaction has a debiting account that is not in the
	 * ledger books.
	 * @exception TransactionError When the ledger book for the debiting account is closed.
	 * @exception TransactionError When the transaction has a crediting account that is not in the
	 * ledger books.
	 * @exception TransactionError When the ledger book for the crediting account is closed.
	 * @exception TransactionError When the transaction is not unreconciled.
	 * @exception AccountError When the transaction is for a currency that is unknown and the
	 * opening account does not exist.
	 * @exception TransactionError When the transaction is for a currency that is unknown.
	 *
	 * @param transaction The transaction that is to be posted.
	 *
	 * @return The transaction number for the posted transaction.
	 */
	TransactionNumber post(const Transaction& transaction);
	/**
	 * @brief Replaces a non-reconciled transaction with a new transaction.
	 *
	 * @exception TransactionError When the new transaction has a debiting account that is not in
	 * the ledger books.
	 * @exception TransactionError When the ledger book for the debiting account is closed.
	 * @exception TransactionError When the new transaction has a crediting account that is not in
	 * the ledger books.
	 * @exception TransactionError When the ledger book for the crediting account is closed.
	 * @exception TransactionError When the supplied original transaction number does not exist.
	 * @exception TransactionError When the original transaction is already reconciled.
	 * @exception TransactionError When the new transaction is not unreconciled.
	 * @exception TransactionError When the new transaction is for a currency that is unknown.
	 *
	 * @param number The transaction number of the original transaction to replace.
	 * @param transaction The new transaction that will replace the original one.
	 */
	void repost(const TransactionNumber& number, const Transaction& transaction);
	/**
	 * @brief Adds a new account to general ledger books.
	 *
	 * @exception AccountError When the account already exists.
	 * @exception AccountError When the account is closed.
	 * @exception AccountError When the account type is not known.
	 * @exception AccountError When the account code is not valid.
	 * @exception AccountError When the parent account type is different than the adding account.
	 *
	 * @param toAdd The new account to add to the ledger books.
	 * @param generator The generator for new account numbers already to be used.
	 *
	 * @return The number of the transaction of the posted opening balance when there is one,
	 * not set when there isn't a balance to post.
	 */
	foundation::Optional<TransactionNumber> add(
		const Account& toAdd,
		accounting::AccountNumberGenerator& generator
	);
	/**
	 * @brief Removes an unused account from the general ledger books.
	 *
	 * @exception AccountError When the account is in use, i.e. has transactions.
	 * @exception AccountError When the account does not exist.
	 * @exception AccountError When the account is closed.
	 * @exception AccountError When the account to remove is the parent of another account.
	 *
	 * @param code The code of the unused account to remove.
	 */
	void remove(const AccountCode& code);
	/**
	 * @brief Closes a ledger.
	 *
	 * @exception AccountError When the account does not exist.
	 * @exception AccountError When the account to close is the parent of another account and the
	 * other account is not closed.
	 *
	 * @param code The account code of the account ledger book to close.
	 */
	void close(const AccountCode& code);
	/**
	 * @brief Opens a ledger.
	 *
	 * @exception AccountError When the account does not exist.
	 * @exception AccountError When the account to open is a child account whose parent is closed.
	 *
	 * @param code The account code of the account ledger book to open.
	 */
	void open(const AccountCode& code);
	/**
	 * @brief Changes the code of a ledger book and recodes all transactions.
	 *
	 * @exception AccountError When the current account does not exist.
	 * @exception AccountError When the account is closed.
	 * @exception AccountError When the changed account code is already in use.
	 * @exception AccountError When the account for a parent does not exist.
	 * @exception AccountError When a parent account refers to a child account.
	 *
	 * @param toRecode The account code of the account ledger book to change.
	 * @param recodeAs The account code of the account ledger book to change to.
	 */
	void recode(const AccountCode& toRecode, const AccountCode& recodeAs);
	/**
	 * @brief Marks the transaction supplied as cleared.
	 *
	 * @exception Error When the transaction number does not exist in the ledger books.
	 *
	 * @param number The number of the transaction to be cleared.
	 */
	void clear(const TransactionNumber& number);
	/**
	 * @brief Marks all the transactions supplied as cleared.
	 *
	 * @exception Error When a transaction number does not exist in the ledger books.
	 *
	 * @param transactionNumbers The numbers of the transactions to be cleared.
	 */
	void clear(const std::vector<TransactionNumber>& transactionNumbers);
	/**
	 * @brief Reconciles the transaction supplied.
	 *
	 * @exception Error When the transaction number does not exist in the ledger books.
	 *
	 * @param number The number of the transaction to be reconciled.
	 */
	void reconcile(const TransactionNumber& number);
	/**
	 * @brief Reconciles all transactions supplied.
	 *
	 * @exception Error When a transaction number does not exist in the ledger books.
	 *
	 * @param transactionNumbers The numbers of the transactions to be reconciled.
	 */
	void reconcile(const std::vector<TransactionNumber>& transactionNumbers);
	/**
	 * @brief Removes the transaction supplied.
	 *
	 * @exception Error When a transaction number does not exist in the ledger books.
	 * @exception TransactionError When a transaction in the ledger books is already reconciled.
	 *
	 * @param number The number of the transaction to be removed.
	 */
	void remove(const TransactionNumber& number);
	/**
	 * @brief Removes all the transactions supplied.
	 *
	 * @exception Error When a transaction number does not exist in the ledger books.
	 * @exception TransactionError When a transaction in the ledger books is already reconciled.
	 *
	 * @param transactionNumbers The numbers of the transactions to be removed.
	 */
	void remove(const std::vector<TransactionNumber>& transactionNumbers);
	/**
	 * @brief Provides all the known accounts in the general ledger.
	 */
	std::set<accounting::Account> accounts() const;
	/**
	 * @brief Provides all the known account codes in the general ledger.
	 */
	std::set<accounting::AccountCode> accountCodes() const;
	/**
	 * @brief Provides all the known account codes in the general ledger of a specific type.
	 *
	 * @param type The type of account to provide the codes for.
	 */
	std::set<accounting::AccountCode> accountCodes(const AccountTypes& type) const;
	/**
	 * @brief Provides the account code for the ledger book tracking the opening account balances.
	 *
	 * @return When set provides the account code in the ledger book.
	 */
	const foundation::Optional<AccountCode>& openingAccountCode() const;
	/**
	 * @brief Determines if the general ledger is empty.
	 *
	 * @return Gives true when there are not any accounts, false otherwise.
	 */
	bool isEmpty() const;
	/**
	 * @brief Converts a ledger account's running balance from one currency into another. If the
	 * opening balance account is not created, it'll be created.
	 *
	 * @exception TransactionError When the supplied currency code to convert to is unknown.
	 * @exception TransactionError When the account to convert is closed.
	 * @exception AccountError When the account to convert does not exist.
	 * @exception AccountError When the account in already in the currency code to convert to.
	 *
	 * @param convertAccount The ledger account to convert.
	 * @param code The currency to convert the ledger account into.
	 * @param generator The generator for new account numbers already to be used.
	 */
	void convertTo(
		const accounting::AccountCode& convertAccount,
		const pecunia::currency::Iso4217Codes& code,
		accounting::AccountNumberGenerator& generator
	);
	/**
	 * @brief Determines if an account can be removed.
	 *
	 * @param code The account code that should be checked.
	 *
	 * @return Gives true when the criteria for removing an account is true, false otherwise.
	 */
	bool canRemove(const accounting::AccountCode& code) const;
	/**
	 * @brief Changes the opening balance account code for future transactions.
	 *
	 * @exception AccountError When the ledger book does not exist for that account code.
	 * @exception AccountError When the account type for that account code is not correct.
	 *
	 * @param code The code to change the account into.
	 */
	void setOpeningAccountCode(foundation::Optional<AccountCode> code);
	/**
	 * @brief Provides the account codes for a requested selection of account numbers.
	 *
	 * @exception AccountError When an account number does not have an account code.
	 *
	 * @param accountNumbers The account numbers to get the account codes for.
	 *
	 * @return The fully populated account codes for the requested account numbers.
	 */
	std::set<AccountCode> accountCodes(const std::set<AccountNumber>& accountNumbers) const;
};

DRN_ACCOUNTING_EXPORT std::ostream& operator<<(std::ostream& o, const LedgerAccount& la);
DRN_ACCOUNTING_EXPORT std::ostream& operator<<(std::ostream& o, const GeneralLedger& gl);
/**
 * @brief Collects all currencies used by the accounts in the general ledger.
 *
 * @param gl The general ledger to collect from.
 *
 * @return All the unique currency codes for the accounts are using.
 */
DRN_ACCOUNTING_EXPORT std::set<pecunia::currency::Iso4217Codes> containedCurrencies(
	const GeneralLedger& gl
);

}}

#endif
