/*******************************************************************************
***  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 "LedgersWidget.h"
using ::QWidget;
using std::function;
using std::set;
using std::vector;
using drn::desktop_ui::internal::LedgersWidget;
using drn::foundation::ObserverPtr;
using drn::foundation::Optional;

#include <pecunia/Codes.h>
using pecunia::currency::Iso4217Codes;
#include <QAbstractButton>
using ::QAbstractButton;
#include <QPushButton>
using ::QPushButton;
#include <QScrollBar>
using ::QScrollBar;
#include <QtDebug>
// using qDebug
#include <QTreeWidgetItem>
using ::QTreeWidgetItem;

#include <algorithm>
using std::find;
using std::remove;
#include <utility>
using std::move;

#include <accounting/Account.h>
using drn::accounting::Account;
using drn::accounting::AccountCode;
using drn::accounting::allAccountTypes;
using drn::accounting::toAccountCode;
#include <accounting/LedgerBooks.h>
using drn::accounting::LedgerAccount;
#include <accounting/Transaction.h>
using drn::accounting::Transaction;
using drn::accounting::TransactionNumber;
using drn::accounting::presentationText;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/QtMemory.hpp>
using drn::foundation::makeQtPtr;
#include "TransactionWidget.h"
using drn::desktop_ui::internal::TransactionWidget;


namespace
{

bool isNewEntry(const TransactionWidget& entry)
{
	return ! entry.number().hasValue();
}

}

Optional<ObserverPtr<TransactionWidget>> LedgersWidget::findTransaction(
	const function<bool(const TransactionWidget&)>& isMatch
) const
{
	for (const auto txnEntry : this->findChildren<TransactionWidget*>())
		if (isMatch(*txnEntry))
			return {txnEntry};
	return {};
}

bool LedgersWidget::anyLedgerDirty() const
{
	return this->findTransaction(
		[] (const auto& txnEntry)
		{
			return txnEntry.isDirty();
		}
	).hasValue();
}

void LedgersWidget::addTransaction(const Transaction& txn)
{
	qDebug() << "Adding Transaction " << presentationText(txn.number().valueOrDefault());

	qint32 foundLocation{0}, checkLocation{1};
	for (const auto txnEntry : this->findChildren<TransactionWidget*>())
	{
		const auto entryOccuredOn{txnEntry->occurredOn()};

		if (
			 ! txn.number().hasValue()
			|| entryOccuredOn < txn.occurredOn()
			|| (entryOccuredOn == txn.occurredOn() && txnEntry->number() < txn.number())
		)
			foundLocation = checkLocation;
		++checkLocation;
	}
	const auto accountCode{this->selectedAccountCode()};
	auto tew{
		makeQtPtr<TransactionWidget>(
			txn,
			accountCode,
			this->accountCodes_,
			this->usableCurrencies_,
			this
		)
	};
	this->entries_[accountCode].emplace_back(tew);
	this->transactionVbxlyt->insertWidget(foundLocation, tew.observed());
}

void LedgersWidget::save()
{
	qDebug() << "Saving Transactions";

	for (auto txnEntry : this->findChildren<TransactionWidget*>())
	{
		qInfo() << "Examining Transaction:" << txnEntry->number().valueOrDefault().integer()
				<< txnEntry->description();
		const auto txnNum{txnEntry->number()};

		try
		{
			if ( ! txnEntry->isDirty())
			{
				qInfo() << "Transaction Not Changed";
				continue;
			}
			else if (txnEntry->shouldRemove())
			{
				qInfo() << "Transaction to be Removed";

				if (txnEntry->number().hasValue())
					emit removedTransaction(*txnNum);
				else
					this->onSaveSucceeded(txnNum, {});
				continue;
			}
			else // Updating or adding
			{
				Transaction txn{
					txnEntry->occurredOn(),
					txnEntry->isDebiting()
						? toAccountCode(txnEntry->account())
						: txnEntry->displayedAccount(),
					txnEntry->isCrediting()
						? toAccountCode(txnEntry->account())
						: txnEntry->displayedAccount(),
					txnEntry->amount(),
					txnEntry->description().trimmed().isEmpty()
						? Optional<QString>{}
						: Optional<QString>{txnEntry->description().trimmed()}
				};

				if ( ! txnNum.hasValue())
				{
					qInfo() << "Transaction to be Added" << txnEntry->description();

					if (txnEntry->isCleared())
						txn.markCleared();

					if (txnEntry->isReconciled())
						txn.markReconciled();
					emit addedTransaction(txn);
				}
				else
				{
					qInfo() << "Transaction to be Updated:" << (*txnNum).integer();
					emit updatedTransaction(*txnNum, txn);

					if (txnEntry->isCleared())
						emit markCleared(*txnNum);

					if (txnEntry->isReconciled())
						emit markReconciled(*txnNum);
				}
			}
		}
		catch (const Error& error)
		{
			this->onSaveFailed(txnEntry->number(), error.message());
		}
	}
	qInfo() << "Processed all ledger transactions.";

	// Checking here in the event the user clicks save, and nothing was modified.
	if ( ! this->anyLedgerDirty())
		this->close();
}

AccountCode LedgersWidget::selectedAccountCode() const
{
	return toAccountCode(this->ledgersTrwdgt->currentItem()->text(0));
}

void LedgersWidget::displayTransactions(const AccountCode& ac)
{
	ObserverPtr<TransactionWidget> lastEntry;

	for (auto accountCodeEntry : this->entries_)
		for (auto txnEntry : accountCodeEntry.second)
			if (accountCodeEntry.first == ac)
			{
				txnEntry->show();

				if (
					lastEntry == nullptr
					|| ! txnEntry->number().hasValue()
					|| lastEntry->occurredOn() < txnEntry->occurredOn()
					|| (
						lastEntry->occurredOn() == txnEntry->occurredOn()
						|| lastEntry->number() < txnEntry->number()
					)
				)
					lastEntry = txnEntry;
			}
			else
				txnEntry->hide();

	if (lastEntry != nullptr)
		this->transactionsScrllr->ensureWidgetVisible(lastEntry.observed());
}

void LedgersWidget::onClicked(ObserverPtr<QAbstractButton> button)
{
	qDebug() << "Handling button clicked" << button->text();

	if (this->saveDiscardDlgbttnbx->button(QDialogButtonBox::Discard) == button)
		this->close();
	else if (this->saveDiscardDlgbttnbx->button(QDialogButtonBox::Save) == button)
		this->save();
	else if (this->addPshbttn_.observed() == button)
	{
		if ( ! this->findTransaction(&isNewEntry).hasValue())
			this->addTransaction({});
		this->displayTransactions(this->selectedAccountCode());
	}
	else
		Q_ASSERT(false && "Unknown button pressed.");
}

void LedgersWidget::handleSelctedAccount(
	ObserverPtr<QTreeWidgetItem> current,
	ObserverPtr<QTreeWidgetItem> previous
)
{
	if (current == nullptr)
		return;

	if (current->text(0).contains(AccountCode::separator_))
	{
		const auto selectedAccount{toAccountCode(current->text(0))};
		const auto previousAccount{
			previous == nullptr || ! previous->text(0).contains(AccountCode::separator_)
				? AccountCode{}
				: toAccountCode(previous->text(0))
		};
		qDebug() << "Requesting to display" << current->text(0) << "previous"
				<< presentationText(previousAccount);
		this->addPshbttn_->setEnabled(true);

		if (this->entries_.count(selectedAccount) == 0)
			emit selectedLedger(selectedAccount);
		else
			this->displayTransactions(selectedAccount);
	}
	else
		this->addPshbttn_->setEnabled(false);
}

void LedgersWidget::onShowLedger(const LedgerAccount& ledger)
{
	qDebug() << "Showing Ledger:" << ledger.account_.code().number().integer();
	Q_ASSERT(this->selectedAccountCode() == ledger.account_.code());

	for (const auto& transaction : ledger.transactions_)
		this->addTransaction(transaction.second);
	this->displayTransactions(ledger.account_.code());
}

bool LedgersWidget::close()
{
	emit closing();
	return QWidget::close();
}

void LedgersWidget::onSaveSucceeded(
	const Optional<TransactionNumber>& displayed,
	const Optional<TransactionNumber>& posted
)
{
	qInfo() << "Succeeded to save transaction" << displayed.valueOrDefault().integer();
	auto txn{
		this->findTransaction(
			[&displayed] (const auto& txnEntry)
			{
				return displayed == txnEntry.number();
			}
		)
	};

	if ( ! txn.hasValue())
	{
		qFatal("Failed to locate the transaction that succeeded to be saved.");
		return;
	}
	auto& entry{*txn};
	entry->saved(posted);

	if (entry->shouldRemove())
	{
		const auto displayedAccount{entry->displayedAccount()};
		auto& accountEntries{this->entries_.at(displayedAccount)};
		accountEntries.erase(
			remove(accountEntries.begin(), accountEntries.end(), entry),
			accountEntries.end()
		);

		if (this->entries_.at(displayedAccount).empty())
			this->entries_.erase(displayedAccount);
		entry->deleteLater();
	}
	qDebug() << "Checking if everything is saved.";

	if ( ! this->anyLedgerDirty())
		this->close();
}

void LedgersWidget::onSaveFailed(const Optional<TransactionNumber>& tn, const QString& errorMessage)
{
	qWarning() << "Failure saving" << tn.valueOrDefault().integer() << errorMessage;
	auto txn{
		this->findTransaction(
			[&tn] (const auto& txnEntry)
			{
				return tn == txnEntry.number();
			}
		)
	};

	if ( ! txn.hasValue())
	{
		qFatal("Failed to locate the transaction that succeeded to be saved.");
		return;
	}
	(*txn)->setErrorMessage(errorMessage);
}

LedgersWidget::LedgersWidget(
	set<Account> accounts,
	set<Iso4217Codes> usableCurrencies,
	const ObserverPtr<QWidget>& parent
) :
	QWidget{parent.observed()},
	addPshbttn_{nullptr},
	accounts_{std::move(accounts)},
	accountCodes_{},
	usableCurrencies_{std::move(usableCurrencies)},
	entries_{}
{
	this->setAttribute(Qt::WA_DeleteOnClose);
	this->setupUi(this);

	for (const auto& accountType : allAccountTypes)
	{
		auto accountTypeTrwdgttm{makeQtPtr<QTreeWidgetItem>(this->ledgersTrwdgt)};
		accountTypeTrwdgttm->setText(0, presentationText(accountType));
		accountTypeTrwdgttm->setFlags(Qt::ItemIsEnabled);

		for (const auto& account : this->accounts_)
			if (account.type() == accountType)
			{
				auto accountTrwdgttm{makeQtPtr<QTreeWidgetItem>(accountTypeTrwdgttm.observed())};
				accountTrwdgttm->setText(0, presentationText(account.code()));
				this->accountCodes_.push_back(account.code());
			}
	}
	this->ledgersTrwdgt->sortItems(0, Qt::SortOrder::AscendingOrder);
	connect(
		this->ledgersTrwdgt,
		&QTreeWidget::QTreeWidget::currentItemChanged,
		this,
		&LedgersWidget::handleSelctedAccount
	);

	//{ Set-up Add Button

	this->addPshbttn_ = saveDiscardDlgbttnbx->addButton(
		tr("&Add"),
		QDialogButtonBox::ButtonRole::ActionRole
	);

	if (this->addPshbttn_ == nullptr)
		throw Error{QObject::tr("Failed to create the add push button.")};
	this->addPshbttn_->setEnabled(false);

	//}

	connect(
		this->saveDiscardDlgbttnbx,
		&QDialogButtonBox::clicked,
		this,
		&LedgersWidget::onClicked
	);
}
