/*******************************************************************************
***  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 "GoalsWidget.h"
using pecunia::currency::Iso4217Codes;
using ::QString;
using ::QWidget;
using std::chrono::hours;
using std::function;
using std::set;
using drn::desktop_ui::internal::GoalsWidget;
using drn::foundation::Days;
using drn::foundation::ObserverPtr;
using drn::foundation::Optional;

#include <pecunia/Math.h>
using pecunia::math::sum;
#include <pecunia/Money.h>
using pecunia::currency::Money;
#include <QAbstractButton>
using ::QAbstractButton;
#include <QDebug>
// using qDebug.
#include <QPushButton>
using ::QPushButton;

#include <stdexcept>
using std::exception;
using std::logic_error;
#include <vector>
using std::vector;

#include <accounting/AccountCode.h>
using drn::accounting::AccountCode;
using drn::accounting::AccountNumber;
#include <banking/BankName.h>
using drn::banking::BankName;
#include <banking/BankAccount.h>
using drn::banking::BankAccount;
#include <banking/BankAccountType.h>
using drn::banking::BankAccountType;
using drn::banking::SupportedAccountTypes;
#include <budgeting/Algorithm.h>
using drn::budgeting::adjustTo;
#include <budgeting/BudgetSource.h>
using drn::budgeting::BudgetSource;
#include <budgeting/EventFrequency.h>
using drn::budgeting::EventFrequency;
#include <budgeting/Goal.h>
using drn::budgeting::Goal;
#include <budgeting/GoalMap.h>
using drn::budgeting::GoalMap;
#include <foundation/Error.h>
using drn::foundation::Error;
#include <foundation/QtMemory.hpp>
using drn::foundation::makeQtPtr;
#include "internal/GoalEntryWidget.h"
using drn::desktop_ui::internal::GoalEntryWidget;
#include <navigation/BudgetBankAccount.h>
using drn::navigation::BudgetBankAccount;
using drn::navigation::lookUpAccount;


namespace
{

bool isGoalType(const SupportedAccountTypes& sat)
{
	switch (sat)
	{
	case SupportedAccountTypes::Chequeing:
	case SupportedAccountTypes::Savings:
		return true;
	case SupportedAccountTypes::Unknown:
	case SupportedAccountTypes::CreditCard:
	case SupportedAccountTypes::Loan:
	case SupportedAccountTypes::Goal:
	default:
		return false;
	}
}

}

Optional<ObserverPtr<GoalEntryWidget>> GoalsWidget::findGoal(
	const function<bool(const GoalEntryWidget&)>& isMatch
)
{
	for (const auto& goalEntry : this->findChildren<GoalEntryWidget*>())
		try
		{
			if (isMatch(*goalEntry))
				return {goalEntry};
		}
		catch (const exception& error)
		{
			qDebug() << "An error occurred while checking a goal entry." << error.what();
			continue;
		}
	return {};
}

void GoalsWidget::addGoal(const Optional<Goal>& g, const Optional<BankAccount>& ba)
{
	qDebug() << "Adding goal entry:" << g.valueOrDefault().source().name();
	auto ew{
		makeQtPtr<GoalEntryWidget>(
			g.valueOrDefault(),
			ba,
			this->banks_,
			this->totalWage_,
			this->hrInWeek_,
			this->dayInWeek_,
			this->usableCurrencies_,
			this
		)
	};
	connect(
		ew.observed(),
		&GoalEntryWidget::valueChanged,
		this,
		&GoalsWidget::updateTotal
	);
	connect(
		ew.observed(),
		&GoalEntryWidget::lookUpBankAccounts,
		this,
		&GoalsWidget::lookUpBankAccounts
	);

	if ( ! g.hasValue())
		this->goalVbxlyt->insertWidget(0, ew.observed());
	else
		this->goalVbxlyt->insertWidget(
			this->goalVbxlyt->indexOf(this->goalBottomSpcr),
			ew.observed()
		);
}

bool GoalsWidget::anyGoalDirty() const
{
	for (const auto& goalEntry : this->findChildren<GoalEntryWidget*>())
		if (goalEntry->isDirty())
			return true;
	return false;
}

void GoalsWidget::addEntry()
{
	qDebug() << "Adding empty goal.";
	this->addGoal({}, {});
}

void GoalsWidget::saveGoals()
{
	qInfo() << "Saving all goals.";

	for (auto& goalEntry : this->findChildren<GoalEntryWidget*>())
	{
		qInfo() << "Examining Goal,"
			<< "Original Source:" << goalEntry->originalSource().valueOrDefault().name();

		try
		{
			if ( ! goalEntry->isDirty())
			{
				qInfo() << goalEntry->source().name() << "Goal Did Not Change.";
				continue;
			}
			else if (goalEntry->shouldRemove())
			{
				qInfo() << "Goal to be Removed";

				if (goalEntry->originalSource().hasValue())
					emit removed(*goalEntry->originalSource());
				// Resetting the dirty flag should the entry be examined before it's deleted.
				goalEntry->resetDirty();
				goalEntry->deleteLater();
				continue;
			}
			else if ( ! goalEntry->originalSource().hasValue())
			{
				qInfo() << "Goal to be Added:" << goalEntry->source().name();
				const Goal goal{
					goalEntry->source(),
					goalEntry->amount(),
					goalEntry->period(),
					goalEntry->nextOccurOn(),
					goalEntry->final()
				};
				const auto ba{
					[&goalEntry] () -> Optional<BankAccount>
					{
						const auto b{goalEntry->bank()};
						const auto a{goalEntry->account()};
						const auto sat{goalEntry->bankAccountType()};

						if (b.hasValue() && a.hasValue() && sat.hasValue())
							return BankAccount{*b, {*a, *sat}};
						return {};
					}()
				};
				emit added(goal, ba);
			}
			else // Must be an update or rename.
			{
				qInfo() << "Goal to be Updated:" << goalEntry->source().name();

				if (goalEntry->originalSource() != goalEntry->source())
					emit renamed(*goalEntry->originalSource(), goalEntry->source());
				const Goal goal{
					goalEntry->source(),
					goalEntry->amount(),
					goalEntry->period(),
					goalEntry->nextOccurOn(),
					goalEntry->final()
				};
				const auto ba{
					[&goalEntry] () -> Optional<BankAccount>
					{
						const auto b{goalEntry->bank()};
						const auto a{goalEntry->account()};
						const auto sat{goalEntry->bankAccountType()};

						if (b.hasValue() && a.hasValue() && sat.hasValue())
							return BankAccount{*b, {*a, *sat}};
						return {};
					}()
				};
				emit updated(goal, ba);
			}
		}
		catch (const Error& e)
		{
			qWarning() << "Error during the save data transformation." << e.what();
			goalEntry->setErrorMessage(e.message());
		}
	}
	qInfo() << "Processed all goals.";

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

void GoalsWidget::updateTotal()
{
	qDebug() << "Updating goals total";
	vector<Money> items{};

	for (const auto& goal : this->findChildren<const GoalEntryWidget*>())
	{
		const Money amount{goal->amount()};
		const EventFrequency period{goal->period()};

		if (
			period == EventFrequency::Unknown
			|| amount.code() == Iso4217Codes::XXX
			|| goal->shouldRemove()
		)
			continue;
		items.emplace_back(
			adjustTo(
				EventFrequency::Monthly,
				amount,
				period,
				this->hrInWeek_,
				this->dayInWeek_
			)
		);
	}
	const auto total{sum(this->preferred_, items)};
	this->totalMnyntr->setMoney(total);
}

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

	if (this->saveDiscardBttnbx->button(QDialogButtonBox::Discard) == button)
		this->close();
	else if (this->saveDiscardBttnbx->button(QDialogButtonBox::Save) == button)
		this->saveGoals();
	else if (this->addPshbttn_.observed() == button)
		this->addEntry();
	else
		throw logic_error{"Unknown button pressed, " + button->text().toStdString() + '.'};
}

bool GoalsWidget::close()
{
	qDebug() << "The goals widget is about to close.";
	emit closing();
	return QWidget::close();
}

void GoalsWidget::onSaveSucceeded(const BudgetSource& source)
{
	qInfo() << "Succeeded to save" << source.name();
	auto goal{
		this->findGoal(
			[&source] (const auto& entry)
			{
				return entry.originalSource() == source
					|| ( ! entry.originalSource().hasValue() && entry.source() == source);
			}
		)
	};

	if ( ! goal.hasValue())
	{
		qFatal("Failed to locate the goal that succeeded to be saved.");
		return;
	}
	(*goal)->saved();
	qDebug() << "Checking if everything is saved.";

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

void GoalsWidget::onSaveFailed(
	const Optional<BudgetSource>& original,
	const QString& errorMessage,
	const Optional<BudgetSource>& displayed
)
{
	qWarning() << "Failed to save,"
			<< "Original Source"<< original.valueOrDefault().name()
			<< "Displayed Source" << displayed.valueOrDefault().name()
			<< errorMessage;
	auto goal{
		this->findGoal(
			[&original, &displayed] (const auto& entry)
			{
				const auto sameOriginal{entry.originalSource() == original};
				const auto sameDisplayed{entry.source() == displayed};
				return sameOriginal && sameDisplayed;
			}
		)
	};

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

void GoalsWidget::onLookedUpBankAccounts(
	const BankName& bn,
	set<BankAccountType> accountTypes
)
{
	auto goal{
		this->findGoal(
			[&bn] (const auto& entry)
			{
				return entry.isRequested(bn);
			}
		)
	};

	if ( ! goal.hasValue())
	{
		qFatal("Failed to locate the goal that requested the bank accounts.");
		return;
	}

	auto accountType{accountTypes.begin()};
	while (accountType != accountTypes.end())
		if ( ! isGoalType(accountType->type_))
			accountType = accountTypes.erase(accountType);
		else
			++accountType;
	(*goal)->setAccountCodes(std::move(accountTypes));
}

GoalsWidget::GoalsWidget(
	const GoalMap& goals,
	const set<BudgetBankAccount>& accounts,
	const Money& totalWage,
	const hours& hrInWeek,
	const Days& dayInWeek,
	set<BankName> banks,
	const Iso4217Codes& preferred,
	const set<Iso4217Codes>& usableCurrencies,
	ObserverPtr<QWidget> parent
) :
	QWidget{parent.observed()},
	totalWage_{totalWage},
	hrInWeek_{hrInWeek},
	dayInWeek_{dayInWeek},
	preferred_{preferred},
	usableCurrencies_{usableCurrencies},
	addPshbttn_{nullptr},
	banks_{std::move(banks)}
{
	this->setAttribute(Qt::WA_DeleteOnClose);
	this->setupUi(this);
	this->totalMnyntr->setUsableCodes(this->usableCurrencies_);
	this->totalMnyntr->setLabel(tr("Monthly Total Amount:"));

	if (goals.empty())
		this->addEntry();
	else
		for (const auto& goal : goals)
			this->addGoal(goal.second, lookUpAccount(goal.first, accounts));
	this->updateTotal();

	//{ Set-up Add Button

	connect(
		this->saveDiscardBttnbx,
		&QDialogButtonBox::clicked,
		this,
		&GoalsWidget::onClicked
	);
	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.")};

	//}
}
