/*******************************************************************************
***  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 "AccountsWidget.h"
using pecunia::currency::Iso4217Codes;
using ::QObject;
using ::QWidget;
using std::function;
using std::set;
using drn::accounting::AccountCode;
using drn::accounting::AccountNumber;
using drn::accounting::presentationText;
using drn::accounting::toAccountCode;
using drn::desktop_ui::internal::AccountsWidget;
using drn::foundation::ObserverPtr;
using drn::foundation::Optional;

#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <QAbstractButton>
using ::QAbstractButton;
#include <QtDebug>
// using qDebug
#include <QIntValidator>
using ::QIntValidator;
#include <QListWidget>
using ::QListWidgetItem;
#include <QPushButton>
using ::QPushButton;

#include <algorithm>
using std::for_each;
using std::transform;
#include <exception>
using std::exception;
#include <iterator>
using std::inserter;
#include <memory>
using std::make_unique;
using std::unique_ptr;
#include <utility>
using std::move;

#include "AccountEntryWidget.h"
using drn::desktop_ui::internal::AccountEntryWidget;
#include <accounting/Account.h>
using drn::accounting::Account;
using drn::accounting::AccountTypes;
using drn::accounting::allAccountTypes;
using drn::accounting::toAccountTypes;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/QtMemory.hpp>
using drn::foundation::makeQtPtr;


namespace
{

bool isCurrentlyDisplayed(const AccountEntryWidget& entry)
{
	return ! entry.isHidden();
}

bool isShowingNewAccountEntry(const AccountEntryWidget& entry)
{
	return entry.isEmptyCode();
}

}

void AccountsWidget::insertAccountEntry(const Optional<Account>& a)
{
	qDebug() << "Inserting account entry." << a.valueOrDefault().code().name();
	auto item{make_unique<QListWidgetItem>(this->accountCodesLstwdgt)};
	item->setText(
		a.hasValue()
			? presentationText(a->code())
			: AccountEntryWidget::newAccountCodeText()
	);
	ObserverPtr<QListWidgetItem> codeItem{item};
	this->accountCodesLstwdgt->addItem(item.release());
	auto widget{
		a.hasValue()
			? makeQtPtr<AccountEntryWidget>(
				a->code(),
				a->isClosed(),
				a->type(),
				a->balance(),
				this->accountNumbers_,
				this->usableCurrencies_,
				this
			)
			: makeQtPtr<AccountEntryWidget>(
				Optional<AccountCode>{},
				false,
				AccountTypes::Unknown,
				Money{},
				this->accountNumbers_,
				this->usableCurrencies_,
				this
			)
	};
	this->accountEntryVbxlyt->insertWidget(0, widget.observed());
	widget->hide();
	this->accountCodesLstwdgt->setCurrentItem(codeItem.observed());
}

void AccountsWidget::displayAccount(ObserverPtr<QListWidgetItem> listItem)
{
	Q_ASSERT(listItem != nullptr && "The observed account cannot be null.");
	Q_ASSERT(
		listItem == this->accountCodesLstwdgt->currentItem() && "It must already be selected."
	);
	qDebug() << "Loading Account" << listItem->text();

	auto current{this->findAccountEntry(&isCurrentlyDisplayed)};

	if (current.hasValue())
		(*current)->hide();
	else
		qDebug("Failed to locate the account that is currently displayed, assuming none are.");
	auto display{
		this->findAccountEntry(
			[listItemText{listItem->text()}] (const auto& entry)
			{
				const bool isNew{listItemText == AccountEntryWidget::newAccountCodeText()};

				if (isNew)
					return entry.number() == AccountNumber::invalid_
						&& entry.name().isEmpty();
				const auto nameNumber{listItemText.split(AccountCode::separator_)};
				Q_ASSERT(nameNumber.size() == 2);
				const bool isSameNumber{nameNumber.at(0).toUInt() == entry.number()};
				const bool isSameName{
					nameNumber.at(1) == entry.name() || entry.name().isEmpty()
				};
				return isSameNumber && isSameName;
			}
		)
	};

	if ( ! display.hasValue())
	{
		qFatal("Failed to locate the account that is to be displayed.");
		return;
	}
	(*display)->show();
}

Optional<ObserverPtr<AccountEntryWidget>> AccountsWidget::findAccountEntry(
	const function<bool(const AccountEntryWidget&)>& isMatch
)
{
	for (const auto& entry : this->findChildren<AccountEntryWidget*>())
		try
		{
			if (isMatch(*entry))
				return {entry};
		}
		catch (const exception& error)
		{
			qDebug() << "An error occurred while checking an account entry." << error.what();
			continue;
		}
	return {};
}

Optional<ObserverPtr<const AccountEntryWidget>> AccountsWidget::findAccountEntry(
	const function<bool(const AccountEntryWidget&)>& isMatch
) const
{
	for (const auto& entry : this->findChildren<const AccountEntryWidget*>())
		try
		{
			if (isMatch(*entry))
				return {entry};
		}
		catch (const exception& error)
		{
			qDebug() << "An error occurred while checking an account entry." << error.what();
			continue;
		}
	return {};
}

void AccountsWidget::save()
{
	qDebug() << "Saving all account modifications.";
	this->accountCodesLstwdgt->setCurrentRow(-1);

	for (auto& entry : this->findChildren<AccountEntryWidget*>())
	{
		const auto currentNumber{entry->number()};
		const auto currentName{entry->name()};
		const auto currentParent{entry->parentNumber()};
		const auto codeText{entry->accountCodeText()};
		const bool isNew{ ! entry->original().hasValue()};

		try
		{
			if ( ! entry->isDirty())
			{
				qDebug() << "Skipping saving non-dirty account" << entry->name();
				continue;
			}

			if ( ! isNew && entry->isClosed() && ! entry->isOriginallyClosed())
			{
				qDebug() << "Closing Account:" << codeText;
				emit closed(AccountCode{AccountNumber{currentNumber}, currentName});
			}
			else if ( ! isNew && ! entry->isClosed() && entry->isOriginallyClosed())
			{
				qDebug() << "Opening Account:" << codeText;
				emit opened(AccountCode{AccountNumber{currentNumber}, currentName});
			}

			if (entry->isRemoved())
			{
				qDebug() << "Removing Account:" << codeText;

				if (isNew)
				{
					this->removeAccountCode(codeText);
					// N.B. resetting dirty flag now as it's now been handled.
					entry->resetDirtyFlag();
					entry->deleteLater();
				}
				else
					emit removed(AccountCode{AccountNumber{currentNumber}, currentName});
				continue;
			}

			if (
				 ! isNew
				&& (
					entry->original()->number().integer() != currentNumber
					|| entry->original()->name() != currentName
					|| (
						(entry->original()->parent().hasValue() && ! currentParent.hasValue())
						|| ( ! entry->original()->parent().hasValue() && currentParent.hasValue())
						|| (
							entry->original()->parent().hasValue()
								&& currentParent.hasValue()
								&& entry->original()->parent()->integer() != *currentParent
						)
					)
				)
			)
			{
				qDebug() << "Recoding Account from"
						<< presentationText(*entry->original()) << "to" << codeText;
				emit recoded(
					*entry->original(),
					AccountCode{
						AccountNumber{currentNumber},
						currentName,
						currentParent.hasValue()
							? AccountNumber{*currentParent}
							: Optional<AccountNumber>{}
					}
				);
			}
			else if (isNew)
			{
				qDebug() << "Adding Account:" << codeText;
				const Account account{
					toAccountTypes(entry->type()),
					AccountCode{
						AccountNumber{currentNumber},
						currentName,
						currentParent.hasValue()
							? AccountNumber{*currentParent}
							: Optional<AccountNumber>{}
					},
					entry->balance(),
					entry->isClosed()
				};
				emit added(account);
			}
		}
		catch (const exception& error)
		{
			qWarning() << "Account entry save error for" << codeText << error.what();
			this->onSaveFailed(
				entry->original(),
				error.what(),
				currentNumber != AccountNumber::invalid_ && ! currentName.isEmpty()
					? AccountCode{AccountNumber{currentNumber}, currentName}
					:
						 ! currentName.isEmpty()
							? AccountCode{currentName}
							: AccountCode{}
			);
			return;
		}
	}
	qInfo() << "Processed all accounts.";

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

bool AccountsWidget::anyAccountDirty() const
{
	return this->findAccountEntry(
		[] (const auto& entry)
		{
			return entry.isDirty();
		}
	).hasValue();
}

void AccountsWidget::removeAccountCode(const QString& accountCode)
{
	for (qint32 row{0} ; row < this->accountCodesLstwdgt->count() ; ++row)
	{
		const auto rowText{this->accountCodesLstwdgt->item(row)->text()};

		if (rowText == accountCode)
		{
			const unique_ptr<QListWidgetItem> item{this->accountCodesLstwdgt->takeItem(row)};
			qDebug() << "Removing" << item->text() << "from the account list.";
			return;
		}
	}
	qFatal("Failed to locate the account code to remove.");
}

void AccountsWidget::onClicked(ObserverPtr<QAbstractButton> button)
{
	qDebug() << "Handling account entry widget clicked button" << button->text();

	if (this->saveDiscardBttnbx->button(QDialogButtonBox::Discard) == button)
		this->close();
	else if (this->saveDiscardBttnbx->button(QDialogButtonBox::Save) == button)
		this->save();
	else if (this->addPshbttn_.observed() == button)
	{
		if ( ! this->findAccountEntry(&isShowingNewAccountEntry).hasValue())
			this->insertAccountEntry({});
	}
	else
		Q_ASSERT(false && "Unknown button pressed.");
}

void AccountsWidget::accountSelectionChanged(
	ObserverPtr<QListWidgetItem> current,
	ObserverPtr<QListWidgetItem> previous
)
{
	if ((nullptr == current) && (nullptr == previous))
		qDebug() << "Changing displayed account from nothing to nothing.";
	else if ((current != nullptr) && (nullptr == previous))
		qDebug() << "Changing displayed account from nothing to" << current->text();
	else if ((nullptr == current) && (previous != nullptr))
		qDebug() << "Changing displayed account from" << previous->text() << "to nothing.";
	else
		qDebug() << "Changing displayed account from" << previous->text() << "to"
			<< current->text();

	if (previous != nullptr)
	{
		const auto currentDisplayed{this->findAccountEntry(&isCurrentlyDisplayed)};

		if ( ! currentDisplayed.hasValue())
		{
			qFatal("Failed to locate the previously selected displayed account.");
			return;
		}
		previous->setText((*currentDisplayed)->accountCodeText());
		const auto number{(*currentDisplayed)->number()};

		if (number != AccountNumber::invalid_)
		{
			const AccountNumber an{number};
			this->accountNumbers_.emplace(an);

			for (auto& entry : this->findChildren<AccountEntryWidget*>())
				entry->addParentableAccountNumber(an);
		}
	}

	if (current != nullptr)
		this->displayAccount(current);
}

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

void AccountsWidget::onSaveSucceeded(const AccountCode& original)
{
	const auto accountCodeText{presentationText(original)};
	qInfo() << "Succeeded to save" << accountCodeText;
	auto foundAccount{
		this->findAccountEntry(
			[&accountCodeText] (const auto& entry)
			{
				return entry.accountCodeText() == accountCodeText
					|| (
						entry.original().hasValue()
							&& presentationText(*entry.original()) == accountCodeText
					);
			}
		)
	};

	if ( ! foundAccount.hasValue())
	{
		qFatal("Failed to locate the account that succeeded to be saved.");
		return;
	}
	auto& widget{*(*foundAccount)};
	widget.saved(original);

	if (widget.isRemoved())
	{
		this->removeAccountCode(accountCodeText);
		widget.deleteLater();
	}
	qDebug() << "Checking if everything is saved.";

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

void AccountsWidget::onSaveFailed(
	const Optional<AccountCode>& original,
	const QString& errorMessage,
	const Optional<AccountCode>& displayed
)
{
	qWarning() << "Failed to save an account entry, Original Code"
			<< presentationText(original.valueOrDefault())
			<< "Displayed Code" << presentationText(displayed.valueOrDefault())
			<< errorMessage;
	auto widget{
		this->findAccountEntry(
			[&original, &displayed] (const auto& entry)
			{
				const auto entryNumber{entry.number()};
				const auto entryName{entry.name()};
				return entryName == displayed.valueOrDefault().name()
					|| (
						entryNumber == original.valueOrDefault().number().integer()
						&&  entryName == original.valueOrDefault().name()
					);
			}
		)
	};

	if ( ! widget.hasValue())
	{
		qFatal("Failed to located the widget entry which had the error.");
		return;
	}
	(*widget)->setErrorMessage(errorMessage);
	const auto displayedAccountCodeText{(*widget)->accountCodeText()};

	for (qint32 row{0} ; row < this->accountCodesLstwdgt->count() ; ++row)
	{
		if (this->accountCodesLstwdgt->item(row)->text() == displayedAccountCodeText)
		{
			this->accountCodesLstwdgt->setCurrentItem(this->accountCodesLstwdgt->item(row));
			break;
		}
	}
}

AccountsWidget::AccountsWidget(
	set<Account> accounts,
	set<Iso4217Codes> usableCurrencies,
	const ObserverPtr<QWidget>& parent
) :
	QWidget{parent.observed()},
	accountNumbers_{},
	usableCurrencies_{::move(usableCurrencies)},
	addPshbttn_{nullptr}
{
	this->setupUi(this);
	transform(
		accounts.begin(),
		accounts.end(),
		inserter(this->accountNumbers_, this->accountNumbers_.begin()),
		[] (const auto& account)
		{
			return account.code().number();
		}
	);

	if (accounts.empty())
		this->insertAccountEntry({});
	else
		for_each(
			accounts.rbegin(),
			accounts.rend(),
			[this] (const auto& account)
			{
				this->insertAccountEntry(account);
			}
		);

	//{ Set-up Add Button

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

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

	//}

	this->displayAccount(this->accountCodesLstwdgt->currentItem());

	connect(
		this->accountCodesLstwdgt,
		&QListWidget::currentItemChanged,
		this,
		&AccountsWidget::accountSelectionChanged
	);
	connect(
		this->saveDiscardBttnbx,
		&QDialogButtonBox::clicked,
		this,
		&AccountsWidget::onClicked
	);
}
